Skip to content

Commit

Permalink
Start extracting claims to own classes
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Jul 22, 2024
1 parent e674ca3 commit a17048a
Show file tree
Hide file tree
Showing 23 changed files with 653 additions and 62 deletions.
1 change: 1 addition & 0 deletions lib/jwt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'jwt/encode'
require 'jwt/error'
require 'jwt/jwk'
require 'jwt/claims'

# JSON Web Token implementation
#
Expand Down
11 changes: 11 additions & 0 deletions lib/jwt/claims.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require_relative 'claims/audience'
require_relative 'claims/expiration'
require_relative 'claims/issued_at'
require_relative 'claims/issuer'
require_relative 'claims/jwt_id'
require_relative 'claims/not_before'
require_relative 'claims/numeric'
require_relative 'claims/required'
require_relative 'claims/subject'
24 changes: 24 additions & 0 deletions lib/jwt/claims/audience.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module JWT
module Claims
class Audience
def initialize(expected_audience:)
@expected_audience = expected_audience
end

def validate!(context:, **_args)
aud = context.payload['aud']
raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
end

def type?(type)
type == :claim
end

private

attr_reader :expected_audience
end
end
end
26 changes: 26 additions & 0 deletions lib/jwt/claims/expiration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module JWT
module Claims
class Expiration
def initialize(leeway:)
@leeway = leeway
end

def validate!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('exp')

raise JWT::ExpiredSignature, 'Signature has expired' if context.payload['exp'].to_i <= (Time.now.to_i - leeway)
end

def type?(type)
type == :claims
end

private

attr_reader :leeway
end
end
end
19 changes: 19 additions & 0 deletions lib/jwt/claims/issued_at.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module JWT
module Claims
class IssuedAt
def validate!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('iat')

iat = context.payload['iat']
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f
end

def type?(type)
type == :claims
end
end
end
end
28 changes: 28 additions & 0 deletions lib/jwt/claims/issuer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module JWT
module Claims
class Issuer
def initialize(issuers:)
@issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
end

def validate!(context:, **_args)
case (iss = context.payload['iss'])
when *issuers
nil
else
raise JWT::InvalidIssuerError, "Invalid issuer. Expected #{issuers}, received #{iss || '<none>'}"
end
end

def type?(type)
type == :claims
end

private

attr_reader :issuers
end
end
end
29 changes: 29 additions & 0 deletions lib/jwt/claims/jwt_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module JWT
module Claims
class JwtId
def initialize(validator:)
@validator = validator
end

def validate!(context:, **_args)
jti = context.payload['jti']
if validator.respond_to?(:call)
verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti)
raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
elsif jti.to_s.strip.empty?
raise(JWT::InvalidJtiError, 'Missing jti')
end
end

def type?(type)
type == :claims
end

private

attr_reader :validator
end
end
end
26 changes: 26 additions & 0 deletions lib/jwt/claims/not_before.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module JWT
module Claims
class NotBefore
def initialize(leeway:)
@leeway = leeway
end

def validate!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('nbf')

raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if context.payload['nbf'].to_i > (Time.now.to_i + leeway)
end

def type?(type)
type == :claims
end

private

attr_reader :leeway
end
end
end
47 changes: 47 additions & 0 deletions lib/jwt/claims/numeric.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module JWT
module Claims
class Numeric
def self.validate!(payload:, **_args)
return unless payload.is_a?(Hash)

new(payload).validate!
end

def self.type?(type)
type == :claims
end

NUMERIC_CLAIMS = %i[
exp
iat
nbf
].freeze

def initialize(payload)
@payload = payload.transform_keys(&:to_sym)
end

def validate!
validate_numeric_claims

true
end

private

def validate_numeric_claims
NUMERIC_CLAIMS.each do |claim|
validate_is_numeric(claim) if @payload.key?(claim)
end
end

def validate_is_numeric(claim)
return if @payload[claim].is_a?(::Numeric)

raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
end
end
end
end
27 changes: 27 additions & 0 deletions lib/jwt/claims/required.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module JWT
module Claims
class Required
def initialize(required_claims:)
@required_claims = required_claims
end

def validate!(context:, **_args)
required_claims.each do |required_claim|
next if context.payload.is_a?(Hash) && context.payload.include?(required_claim)

raise JWT::MissingRequiredClaim, "Missing required claim #{required_claim}"
end
end

def type?(type)
type == :claims
end

private

attr_reader :required_claims
end
end
end
24 changes: 24 additions & 0 deletions lib/jwt/claims/subject.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module JWT
module Claims
class Subject
def initialize(expected_subject:)
@expected_subject = expected_subject.to_s
end

def validate!(context:, **_args)
sub = context.payload['sub']
raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || '<none>'}") unless sub.to_s == expected_subject
end

def type?(type)
type == :claims
end

private

attr_reader :expected_subject
end
end
end
37 changes: 0 additions & 37 deletions lib/jwt/claims_validator.rb

This file was deleted.

3 changes: 1 addition & 2 deletions lib/jwt/encode.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require_relative 'jwa'
require_relative 'claims_validator'

# JWT::Encode module
module JWT
Expand Down Expand Up @@ -55,7 +54,7 @@ def signature
def validate_claims!
return unless @payload.is_a?(Hash)

ClaimsValidator.new(@payload).validate!
Claims::Numeric.new(@payload).validate!
end

def encode_signature
Expand Down
6 changes: 3 additions & 3 deletions lib/jwt/verify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ def initialize(payload, options)
def verify_aud
return unless (options_aud = @options[:aud])

aud = @payload['aud']
raise JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{aud || '<none>'}" if ([*aud] & [*options_aud]).empty?
Claims::Audience.new(expected_audience: options_aud).validate!(context: { payload: @payload })
end

def verify_expiration
return unless contains_key?(@payload, 'exp')
raise JWT::ExpiredSignature, 'Signature has expired' if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)

Claims::Expiration.new(leeway: exp_leeway).validate!(context: { payload: @payload })
end

def verify_iat
Expand Down
Loading

0 comments on commit a17048a

Please sign in to comment.