Skip to content

Commit 487ee12

Browse files
committed
Extract EncodedToken internals into ClaimsContext, SegmentParser, and SignatureVerifier classes to reduce class complexity
1 parent 9560a4e commit 487ee12

4 files changed

Lines changed: 114 additions & 122 deletions

File tree

lib/jwt/encoded_token.rb

Lines changed: 42 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# frozen_string_literal: true
22

3+
require_relative 'encoded_token/claims_context'
4+
require_relative 'encoded_token/segment_parser'
5+
require_relative 'encoded_token/signature_verifier'
6+
37
module JWT
48
# Represents an encoded JWT token
59
#
@@ -11,31 +15,26 @@ module JWT
1115
# encoded_token = JWT::EncodedToken.new(token.jwt)
1216
# encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret')
1317
# encoded_token.payload # => {'pay' => 'load'}
14-
class EncodedToken # rubocop:disable Metrics/ClassLength
15-
# @private
16-
# Allow access to the unverified payload for claim verification.
17-
class ClaimsContext
18-
extend Forwardable
19-
20-
def_delegators :@token, :header, :unverified_payload
21-
22-
def initialize(token)
23-
@token = token
24-
end
25-
26-
def payload
27-
unverified_payload
28-
end
29-
end
30-
18+
class EncodedToken
3119
DEFAULT_CLAIMS = [:exp].freeze
32-
3320
private_constant(:DEFAULT_CLAIMS)
3421

3522
# Returns the original token provided to the class.
3623
# @return [String] The JWT token.
3724
attr_reader :jwt
3825

26+
# Returns the encoded signature of the JWT token.
27+
# @return [String] the encoded signature.
28+
attr_reader :encoded_signature
29+
30+
# Returns the encoded header of the JWT token.
31+
# @return [String] the encoded header.
32+
attr_reader :encoded_header
33+
34+
# Sets or returns the encoded payload of the JWT token.
35+
# @return [String] the encoded payload.
36+
attr_accessor :encoded_payload
37+
3938
# Initializes a new EncodedToken instance.
4039
#
4140
# @param jwt [String] the encoded JWT token.
@@ -46,165 +45,109 @@ def initialize(jwt)
4645
@jwt = jwt
4746
@allow_duplicate_keys = true
4847
@signature_verified = false
49-
@claims_verified = false
50-
48+
@claims_verified = false
5149
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
5250
end
5351

5452
# Enables strict duplicate key detection for this token.
55-
# When called, the token will raise JWT::DuplicateKeyError if duplicate keys
56-
# are found in the header or payload during parsing.
57-
#
58-
# @example
59-
# token = JWT::EncodedToken.new(jwt_string)
60-
# token.raise_on_duplicate_keys!
61-
# token.header # May raise JWT::DuplicateKeyError
62-
#
6353
# @return [self]
64-
# @raise [JWT::DuplicateKeyError] if duplicate keys are found during subsequent parsing.
6554
# @raise [JWT::UnsupportedError] if the JSON gem version does not support duplicate key detection.
6655
def raise_on_duplicate_keys!
6756
raise JWT::UnsupportedError, 'Duplicate key detection requires JSON gem >= 2.13.0' unless JSON.supports_duplicate_key_detection?
6857

6958
@allow_duplicate_keys = false
59+
@parser = nil
7060
self
7161
end
7262

7363
# Returns the decoded signature of the JWT token.
74-
#
7564
# @return [String] the decoded signature.
7665
def signature
7766
@signature ||= ::JWT::Base64.url_decode(encoded_signature || '')
7867
end
7968

80-
# Returns the encoded signature of the JWT token.
81-
#
82-
# @return [String] the encoded signature.
83-
attr_reader :encoded_signature
84-
8569
# Returns the decoded header of the JWT token.
86-
#
8770
# @return [Hash] the header.
8871
def header
89-
@header ||= parse_and_decode(@encoded_header)
72+
@header ||= parser.parse_and_decode(@encoded_header)
9073
end
9174

92-
# Returns the encoded header of the JWT token.
93-
#
94-
# @return [String] the encoded header.
95-
attr_reader :encoded_header
96-
9775
# Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
98-
#
9976
# @return [Hash] the payload.
100-
# @raise [JWT::DecodeError] if the signature has not been verified.
77+
# @raise [JWT::DecodeError] if the signature or claims have not been verified.
10178
def payload
10279
raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified
10380
raise JWT::DecodeError, 'Verify the token claims before accessing the payload' unless @claims_verified
10481

105-
decoded_payload
82+
unverified_payload
10683
end
10784

10885
# Returns the payload of the JWT token without requiring the signature to have been verified.
10986
# @return [Hash] the payload.
11087
def unverified_payload
111-
decoded_payload
88+
@unverified_payload ||= decode_payload
11289
end
11390

114-
# Sets or returns the encoded payload of the JWT token.
115-
#
116-
# @return [String] the encoded payload.
117-
attr_accessor :encoded_payload
118-
11991
# Returns the signing input of the JWT token.
120-
#
12192
# @return [String] the signing input.
12293
def signing_input
12394
[encoded_header, encoded_payload].join('.')
12495
end
12596

12697
# Verifies the token signature and claims.
127-
# By default it verifies the 'exp' claim.
128-
#
129-
# @example
130-
# encoded_token.verify!(signature: { algorithm: 'HS256', key: 'secret' }, claims: [:exp])
131-
#
132-
# @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
133-
# @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
98+
# @param signature [Hash] the parameters for signature verification.
99+
# @param claims [Array<Symbol>, Hash] the claims to verify.
134100
# @return [nil]
135-
# @raise [JWT::DecodeError] if the signature or claim verification fails.
136101
def verify!(signature:, claims: nil)
137102
verify_signature!(**signature)
138103
claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims)
139104
nil
140105
end
141106

142107
# Verifies the token signature and claims.
143-
# By default it verifies the 'exp' claim.
144-
145-
# @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
146-
# @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
147-
# @return [Boolean] true if the signature and claims are valid, false otherwise.
108+
# @param signature [Hash] the parameters for signature verification.
109+
# @param claims [Array<Symbol>, Hash] the claims to verify.
110+
# @return [Boolean] true if the signature and claims are valid.
148111
def valid?(signature:, claims: nil)
149-
valid_signature?(**signature) &&
150-
(claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims))
112+
valid_signature?(**signature) && (claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims))
151113
end
152114

153115
# Verifies the signature of the JWT token.
154-
#
155-
# @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
156-
# @param key [String, Array<String>] the key(s) to use for verification.
157-
# @param key_finder [#call] an object responding to `call` to find the key for verification.
158116
# @return [nil]
159117
# @raise [JWT::VerificationError] if the signature verification fails.
160-
# @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
161118
def verify_signature!(algorithm:, key: nil, key_finder: nil)
162119
return if valid_signature?(algorithm: algorithm, key: key, key_finder: key_finder)
163120

164121
raise JWT::VerificationError, 'Signature verification failed'
165122
end
166123

167124
# Checks if the signature of the JWT token is valid.
168-
#
169-
# @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
170-
# @param key [String, Array<String>, JWT::JWK::KeyBase, Array<JWT::JWK::KeyBase>] the key(s) to use for verification.
171-
# @param key_finder [#call] an object responding to `call` to find the key for verification.
172-
# @return [Boolean] true if the signature is valid, false otherwise.
125+
# @return [Boolean] true if the signature is valid.
173126
def valid_signature?(algorithm: nil, key: nil, key_finder: nil)
174-
raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
175-
176-
keys = Array(key || key_finder.call(self))
177-
verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: header['alg'])
178-
179-
raise JWT::VerificationError, 'No algorithm provided' if verifiers.empty?
180-
181-
valid = verifiers.any? do |jwa|
182-
jwa.verify(data: signing_input, signature: signature)
127+
SignatureVerifier.new(self).verify(algorithm: algorithm, key: key, key_finder: key_finder).tap do |valid|
128+
@signature_verified = valid
183129
end
184-
valid.tap { |verified| @signature_verified = verified }
185130
end
186131

187132
# Verifies the claims of the token.
188-
# @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
133+
# @param options [Array<Symbol>, Hash] the claims to verify.
189134
# @raise [JWT::DecodeError] if the claims are invalid.
190135
def verify_claims!(*options)
191-
Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
192-
@claims_verified = true
193-
end
136+
Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap { @claims_verified = true }
194137
rescue StandardError
195138
@claims_verified = false
196139
raise
197140
end
198141

199142
# Returns the errors of the claims of the token.
200-
# @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
143+
# @param options [Array<Symbol>, Hash] the claims to verify.
201144
# @return [Array<Symbol>] the errors of the claims.
202145
def claim_errors(*options)
203146
Claims::Verifier.errors(ClaimsContext.new(self), *claims_options(options))
204147
end
205148

206149
# Returns whether the claims of the token are valid.
207-
# @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
150+
# @param options [Array<Symbol>, Hash] the claims to verify.
208151
# @return [Boolean] whether the claims are valid.
209152
def valid_claims?(*options)
210153
claim_errors(*claims_options(options)).empty?.tap { |verified| @claims_verified = verified }
@@ -215,42 +158,19 @@ def valid_claims?(*options)
215158
private
216159

217160
def claims_options(options)
218-
return DEFAULT_CLAIMS if options.first.nil?
161+
options.first.nil? ? DEFAULT_CLAIMS : options
162+
end
219163

220-
options
164+
def parser
165+
@parser ||= SegmentParser.new(allow_duplicate_keys: @allow_duplicate_keys)
221166
end
222167

223168
def decode_payload
224169
raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
225170

226-
if unencoded_payload?
227-
verify_claims!(crit: ['b64'])
228-
return parse_unencoded(encoded_payload)
229-
end
230-
231-
parse_and_decode(encoded_payload)
232-
end
233-
234-
def unencoded_payload?
235-
header['b64'] == false
236-
end
237-
238-
def parse_and_decode(segment)
239-
parse(::JWT::Base64.url_decode(segment || ''))
240-
end
241-
242-
def parse_unencoded(segment)
243-
parse(segment)
244-
end
245-
246-
def parse(segment)
247-
JWT::JSON.parse(segment, allow_duplicate_keys: @allow_duplicate_keys)
248-
rescue ::JSON::ParserError
249-
raise JWT::DecodeError, 'Invalid segment encoding'
250-
end
171+
return parser.parse_unencoded(encoded_payload).tap { verify_claims!(crit: ['b64']) } if header['b64'] == false
251172

252-
def decoded_payload
253-
@decoded_payload ||= decode_payload
173+
parser.parse_and_decode(encoded_payload)
254174
end
255175
end
256176
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
class EncodedToken
5+
# @private
6+
# Allow access to the unverified payload for claim verification.
7+
class ClaimsContext
8+
extend Forwardable
9+
10+
def_delegators :@token, :header, :unverified_payload
11+
12+
def initialize(token)
13+
@token = token
14+
end
15+
16+
def payload
17+
unverified_payload
18+
end
19+
end
20+
end
21+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
class EncodedToken
5+
# @private
6+
# Handles segment parsing and duplicate key detection.
7+
class SegmentParser
8+
def initialize(allow_duplicate_keys:)
9+
@allow_duplicate_keys = allow_duplicate_keys
10+
end
11+
12+
def parse_and_decode(segment)
13+
parse(::JWT::Base64.url_decode(segment || ''))
14+
end
15+
16+
def parse_unencoded(segment)
17+
parse(segment)
18+
end
19+
20+
def parse(segment)
21+
JWT::JSON.parse(segment, allow_duplicate_keys: @allow_duplicate_keys)
22+
rescue ::JSON::ParserError
23+
raise JWT::DecodeError, 'Invalid segment encoding'
24+
end
25+
end
26+
end
27+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
class EncodedToken
5+
# @private
6+
# Handles signature verification logic.
7+
class SignatureVerifier
8+
def initialize(token)
9+
@token = token
10+
end
11+
12+
def verify(algorithm:, key: nil, key_finder: nil)
13+
raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
14+
15+
keys = Array(key || key_finder.call(@token))
16+
verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: @token.header['alg'])
17+
18+
raise JWT::VerificationError, 'No algorithm provided' if verifiers.empty?
19+
20+
verifiers.any? { |jwa| jwa.verify(data: @token.signing_input, signature: @token.signature) }
21+
end
22+
end
23+
end
24+
end

0 commit comments

Comments
 (0)