Skip to content

Commit 5f57d96

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

File tree

4 files changed

+107
-88
lines changed

4 files changed

+107
-88
lines changed

lib/jwt/encoded_token.rb

Lines changed: 35 additions & 88 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,8 +45,7 @@ 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

@@ -67,57 +65,39 @@ def raise_on_duplicate_keys!
6765
raise JWT::UnsupportedError, 'Duplicate key detection requires JSON gem >= 2.13.0' unless JSON.supports_duplicate_key_detection?
6866

6967
@allow_duplicate_keys = false
68+
@parser = nil
7069
self
7170
end
7271

7372
# Returns the decoded signature of the JWT token.
74-
#
7573
# @return [String] the decoded signature.
7674
def signature
7775
@signature ||= ::JWT::Base64.url_decode(encoded_signature || '')
7876
end
7977

80-
# Returns the encoded signature of the JWT token.
81-
#
82-
# @return [String] the encoded signature.
83-
attr_reader :encoded_signature
84-
8578
# Returns the decoded header of the JWT token.
86-
#
8779
# @return [Hash] the header.
8880
def header
89-
@header ||= parse_and_decode(@encoded_header)
81+
@header ||= parser.parse_and_decode(@encoded_header)
9082
end
9183

92-
# Returns the encoded header of the JWT token.
93-
#
94-
# @return [String] the encoded header.
95-
attr_reader :encoded_header
96-
9784
# Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
98-
#
9985
# @return [Hash] the payload.
100-
# @raise [JWT::DecodeError] if the signature has not been verified.
86+
# @raise [JWT::DecodeError] if the signature or claims have not been verified.
10187
def payload
10288
raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified
10389
raise JWT::DecodeError, 'Verify the token claims before accessing the payload' unless @claims_verified
10490

105-
decoded_payload
91+
unverified_payload
10692
end
10793

10894
# Returns the payload of the JWT token without requiring the signature to have been verified.
10995
# @return [Hash] the payload.
11096
def unverified_payload
111-
decoded_payload
97+
@unverified_payload ||= decode_payload
11298
end
11399

114-
# Sets or returns the encoded payload of the JWT token.
115-
#
116-
# @return [String] the encoded payload.
117-
attr_accessor :encoded_payload
118-
119100
# Returns the signing input of the JWT token.
120-
#
121101
# @return [String] the signing input.
122102
def signing_input
123103
[encoded_header, encoded_payload].join('.')
@@ -141,13 +121,12 @@ def verify!(signature:, claims: nil)
141121

142122
# Verifies the token signature and claims.
143123
# By default it verifies the 'exp' claim.
144-
124+
#
145125
# @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
146126
# @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
147127
# @return [Boolean] true if the signature and claims are valid, false otherwise.
148128
def valid?(signature:, claims: nil)
149-
valid_signature?(**signature) &&
150-
(claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims))
129+
valid_signature?(**signature) && (claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims))
151130
end
152131

153132
# Verifies the signature of the JWT token.
@@ -171,26 +150,17 @@ def verify_signature!(algorithm:, key: nil, key_finder: nil)
171150
# @param key_finder [#call] an object responding to `call` to find the key for verification.
172151
# @return [Boolean] true if the signature is valid, false otherwise.
173152
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)
153+
SignatureVerifier.new(self).verify(algorithm: algorithm, key: key, key_finder: key_finder).tap do |valid|
154+
@signature_verified = valid
183155
end
184-
valid.tap { |verified| @signature_verified = verified }
185156
end
186157

187158
# Verifies the claims of the token.
188159
# @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
160+
# @return [nil]
189161
# @raise [JWT::DecodeError] if the claims are invalid.
190162
def verify_claims!(*options)
191-
Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
192-
@claims_verified = true
193-
end
163+
Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap { @claims_verified = true }
194164
rescue StandardError
195165
@claims_verified = false
196166
raise
@@ -215,42 +185,19 @@ def valid_claims?(*options)
215185
private
216186

217187
def claims_options(options)
218-
return DEFAULT_CLAIMS if options.first.nil?
188+
options.first.nil? ? DEFAULT_CLAIMS : options
189+
end
219190

220-
options
191+
def parser
192+
@parser ||= SegmentParser.new(allow_duplicate_keys: @allow_duplicate_keys)
221193
end
222194

223195
def decode_payload
224196
raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
225197

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
198+
return parser.parse_unencoded(encoded_payload).tap { verify_claims!(crit: ['b64']) } if header['b64'] == false
251199

252-
def decoded_payload
253-
@decoded_payload ||= decode_payload
200+
parser.parse_and_decode(encoded_payload)
254201
end
255202
end
256203
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)