Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e6652b7

Browse files
committedAug 4, 2024
Move claims into their own classes
1 parent 447802c commit e6652b7

27 files changed

+698
-517
lines changed
 

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
**Fixes and enhancements:**
1212

13+
- Refactor claim validators into their own classes [#605](https://github.com/jwt/ruby-jwt/pull/605) ([@anakinj](https://github.com/anakinj))
1314
- Your contribution here
1415

1516
## [v2.8.2](https://github.com/jwt/ruby-jwt/tree/v2.8.2) (2024-06-18)

‎lib/jwt.rb

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'jwt/encode'
1010
require 'jwt/error'
1111
require 'jwt/jwk'
12+
require 'jwt/claims'
1213

1314
# JSON Web Token implementation
1415
#

‎lib/jwt/claims.rb

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
DEFAULTS = {
6+
leeway: 0
7+
}.freeze
8+
9+
ClaimsContext = Struct.new(:payload, keyword_init: true)
10+
11+
class << self
12+
def verify!(payload, options)
13+
options = DEFAULTS.merge(options)
14+
verify_aud(payload, options)
15+
verify_expiration(payload, options)
16+
verify_iat(payload, options)
17+
verify_iss(payload, options)
18+
verify_jti(payload, options)
19+
verify_not_before(payload, options)
20+
verify_sub(payload, options)
21+
verify_required_claims(payload, options)
22+
end
23+
24+
def verify_aud(payload, options)
25+
return unless options[:verify_aud]
26+
27+
Claims::Audience.new(expected_audience: options[:aud]).validate!(context: ClaimsContext.new(payload: payload))
28+
end
29+
30+
def verify_expiration(payload, options)
31+
return unless options[:verify_expiration]
32+
33+
Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]).validate!(context: ClaimsContext.new(payload: payload))
34+
end
35+
36+
def verify_iat(payload, options)
37+
return unless options[:verify_iat]
38+
39+
Claims::IssuedAt.new.validate!(context: ClaimsContext.new(payload: payload))
40+
end
41+
42+
def verify_iss(payload, options)
43+
return unless options[:verify_iss]
44+
45+
Claims::Issuer.new(issuers: options[:iss]).validate!(context: ClaimsContext.new(payload: payload))
46+
end
47+
48+
def verify_jti(payload, options)
49+
return unless options[:verify_jti]
50+
51+
Claims::JwtId.new(validator: options[:verify_jti]).validate!(context: ClaimsContext.new(payload: payload))
52+
end
53+
54+
def verify_not_before(payload, options)
55+
return unless options[:verify_not_before]
56+
57+
Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]).validate!(context: ClaimsContext.new(payload: payload))
58+
end
59+
60+
def verify_sub(payload, options)
61+
return unless options[:verify_sub]
62+
return unless options[:sub]
63+
64+
Claims::Subject.new(expected_subject: options[:sub]).validate!(context: ClaimsContext.new(payload: payload))
65+
end
66+
67+
def verify_required_claims(payload, options)
68+
return unless (options_required_claims = options[:required_claims])
69+
70+
Claims::Required.new(required_claims: options_required_claims).validate!(context: ClaimsContext.new(payload: payload))
71+
end
72+
end
73+
end
74+
end
75+
76+
require_relative 'claims/audience'
77+
require_relative 'claims/expiration'
78+
require_relative 'claims/issued_at'
79+
require_relative 'claims/issuer'
80+
require_relative 'claims/jwt_id'
81+
require_relative 'claims/not_before'
82+
require_relative 'claims/numeric'
83+
require_relative 'claims/required'
84+
require_relative 'claims/subject'

‎lib/jwt/claims/audience.rb

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class Audience
6+
def initialize(expected_audience:)
7+
@expected_audience = expected_audience
8+
end
9+
10+
def validate!(context:, **_args)
11+
aud = context.payload['aud']
12+
raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
13+
end
14+
15+
private
16+
17+
attr_reader :expected_audience
18+
end
19+
end
20+
end

‎lib/jwt/claims/expiration.rb

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class Expiration
6+
def initialize(leeway:)
7+
@leeway = leeway
8+
end
9+
10+
def validate!(context:, **_args)
11+
return unless context.payload.is_a?(Hash)
12+
return unless context.payload.key?('exp')
13+
14+
raise JWT::ExpiredSignature, 'Signature has expired' if context.payload['exp'].to_i <= (Time.now.to_i - leeway)
15+
end
16+
17+
private
18+
19+
attr_reader :leeway
20+
end
21+
end
22+
end

‎lib/jwt/claims/issued_at.rb

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class IssuedAt
6+
def validate!(context:, **_args)
7+
return unless context.payload.is_a?(Hash)
8+
return unless context.payload.key?('iat')
9+
10+
iat = context.payload['iat']
11+
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f
12+
end
13+
end
14+
end
15+
end

‎lib/jwt/claims/issuer.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class Issuer
6+
def initialize(issuers:)
7+
@issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
8+
end
9+
10+
def validate!(context:, **_args)
11+
case (iss = context.payload['iss'])
12+
when *issuers
13+
nil
14+
else
15+
raise JWT::InvalidIssuerError, "Invalid issuer. Expected #{issuers}, received #{iss || '<none>'}"
16+
end
17+
end
18+
19+
private
20+
21+
attr_reader :issuers
22+
end
23+
end
24+
end

‎lib/jwt/claims/jwt_id.rb

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class JwtId
6+
def initialize(validator:)
7+
@validator = validator
8+
end
9+
10+
def validate!(context:, **_args)
11+
jti = context.payload['jti']
12+
if validator.respond_to?(:call)
13+
verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti)
14+
raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
15+
elsif jti.to_s.strip.empty?
16+
raise(JWT::InvalidJtiError, 'Missing jti')
17+
end
18+
end
19+
20+
private
21+
22+
attr_reader :validator
23+
end
24+
end
25+
end

‎lib/jwt/claims/not_before.rb

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class NotBefore
6+
def initialize(leeway:)
7+
@leeway = leeway
8+
end
9+
10+
def validate!(context:, **_args)
11+
return unless context.payload.is_a?(Hash)
12+
return unless context.payload.key?('nbf')
13+
14+
raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if context.payload['nbf'].to_i > (Time.now.to_i + leeway)
15+
end
16+
17+
private
18+
19+
attr_reader :leeway
20+
end
21+
end
22+
end

‎lib/jwt/claims/numeric.rb

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class Numeric
6+
def self.validate!(payload:, **_args)
7+
return unless payload.is_a?(Hash)
8+
9+
new(payload).validate!
10+
end
11+
12+
NUMERIC_CLAIMS = %i[
13+
exp
14+
iat
15+
nbf
16+
].freeze
17+
18+
def initialize(payload)
19+
@payload = payload.transform_keys(&:to_sym)
20+
end
21+
22+
def validate!
23+
validate_numeric_claims
24+
25+
true
26+
end
27+
28+
private
29+
30+
def validate_numeric_claims
31+
NUMERIC_CLAIMS.each do |claim|
32+
validate_is_numeric(claim) if @payload.key?(claim)
33+
end
34+
end
35+
36+
def validate_is_numeric(claim)
37+
return if @payload[claim].is_a?(::Numeric)
38+
39+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
40+
end
41+
end
42+
end
43+
end

‎lib/jwt/claims/required.rb

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class Required
6+
def initialize(required_claims:)
7+
@required_claims = required_claims
8+
end
9+
10+
def validate!(context:, **_args)
11+
required_claims.each do |required_claim|
12+
next if context.payload.is_a?(Hash) && context.payload.include?(required_claim)
13+
14+
raise JWT::MissingRequiredClaim, "Missing required claim #{required_claim}"
15+
end
16+
end
17+
18+
private
19+
20+
attr_reader :required_claims
21+
end
22+
end
23+
end

‎lib/jwt/claims/subject.rb

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
class Subject
6+
def initialize(expected_subject:)
7+
@expected_subject = expected_subject.to_s
8+
end
9+
10+
def validate!(context:, **_args)
11+
sub = context.payload['sub']
12+
raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || '<none>'}") unless sub.to_s == expected_subject
13+
end
14+
15+
private
16+
17+
attr_reader :expected_subject
18+
end
19+
end
20+
end

‎lib/jwt/claims_validator.rb

-37
This file was deleted.

‎lib/jwt/decode.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

33
require 'json'
4-
5-
require 'jwt/verify'
64
require 'jwt/x5c_key_finder'
75

86
# JWT::Decode module
@@ -113,8 +111,7 @@ def find_key(&keyfinder)
113111
end
114112

115113
def verify_claims
116-
Verify.verify_claims(payload, @options)
117-
Verify.verify_required_claims(payload, @options)
114+
Claims.verify!(payload, @options)
118115
end
119116

120117
def validate_segment_count!

‎lib/jwt/encode.rb

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require_relative 'jwa'
4-
require_relative 'claims_validator'
54

65
# JWT::Encode module
76
module JWT
@@ -55,7 +54,7 @@ def signature
5554
def validate_claims!
5655
return unless @payload.is_a?(Hash)
5756

58-
ClaimsValidator.new(@payload).validate!
57+
Claims::Numeric.new(@payload).validate!
5958
end
6059

6160
def encode_signature
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.