Skip to content

Commit 757d13f

Browse files
committed
Decode class rewritten to increase understanding and to soon support custom algorithms
1 parent d7ef2c0 commit 757d13f

File tree

8 files changed

+254
-59
lines changed

8 files changed

+254
-59
lines changed

lib/jwt.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'base64'
44
require 'jwt/extension'
5+
require 'jwt/decode_token'
56
require 'jwt/json'
67
require 'jwt/decode'
78
require 'jwt/default_options'

lib/jwt/decode.rb

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -123,44 +123,20 @@ def algorithm
123123
end
124124

125125
def header
126-
@header ||= decode_and_parse_header(@segments[0])
126+
@header ||= parse_and_decode @segments[0]
127127
end
128128

129129
def payload
130-
@payload ||= decode_and_parse_payload(@segments[1])
130+
@payload ||= parse_and_decode @segments[1]
131131
end
132132

133133
def signing_input
134134
@segments.first(2).join('.')
135135
end
136136

137-
def decode_and_parse_header(raw_header)
138-
json_parse(decode_header(raw_header))
139-
end
140-
141-
def decode_and_parse_payload(raw_payload)
142-
decode_payload(raw_payload)
143-
end
144-
145-
def decode_payload(raw_segment)
146-
if @options[:decode_payload_proc]
147-
@options[:decode_payload_proc].call(raw_segment, header, @signature)
148-
else
149-
json_parse(Base64.urlsafe_decode64(raw_segment))
150-
end
151-
rescue ArgumentError
152-
raise JWT::DecodeError, 'Invalid segment encoding'
153-
end
154-
155-
def decode_header(raw_segment)
156-
Base64.urlsafe_decode64(raw_segment)
157-
rescue ArgumentError
158-
raise JWT::DecodeError, 'Invalid segment encoding'
159-
end
160-
161-
def json_parse(decoded_segment)
162-
JWT::JSON.parse(decoded_segment)
163-
rescue ::JSON::ParserError
137+
def parse_and_decode(segment)
138+
JWT::JSON.parse(Base64.urlsafe_decode64(segment))
139+
rescue ::JSON::ParserError, ArgumentError
164140
raise JWT::DecodeError, 'Invalid segment encoding'
165141
end
166142
end

lib/jwt/decode_token.rb

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# frozen_string_literal: true
2+
3+
require 'jwt/signature'
4+
require 'jwt/verify'
5+
require 'jwt/x5c_key_finder'
6+
7+
module JWT
8+
class DecodeToken
9+
def initialize(token, options = {})
10+
raise ArgumentError, 'Provided token is not a String object' unless token.is_a?(String)
11+
12+
@token = token
13+
@options = options
14+
end
15+
16+
def decoded_segments
17+
validate_segment_count!
18+
19+
if verify?
20+
verify_alg_header!
21+
verify_signature!
22+
verify_claims!
23+
end
24+
25+
[payload, header]
26+
end
27+
28+
private
29+
30+
attr_reader :token, :options
31+
32+
def algorithms
33+
@algorithms ||= Array(options[:algorithms])
34+
end
35+
36+
def segments
37+
@segments ||= token.split('.')
38+
end
39+
40+
def signature
41+
@signature ||= Base64.urlsafe_decode64(segments[2] || '')
42+
end
43+
44+
def header
45+
@header ||= decode_header(segments[0])
46+
end
47+
48+
def payload
49+
@payload ||= decode_payload(segments[1])
50+
end
51+
52+
def signing_input
53+
segments.first(2).join('.')
54+
end
55+
56+
def verify?
57+
options[:verify] != false
58+
end
59+
60+
def key
61+
@key ||=
62+
if options[:jwks]
63+
::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid'])
64+
elsif (x5c_options = options[:x5c])
65+
::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
66+
else
67+
options[:key]
68+
end
69+
end
70+
71+
def verify_alg_header!
72+
return unless valid_algorithms.empty?
73+
74+
raise JWT::IncorrectAlgorithm, 'Expected a different algorithm'
75+
end
76+
77+
def valid_algorithms
78+
@valid_algorithms ||= algorithms.select do |algorithm|
79+
if algorithm.is_a?(String)
80+
header['alg'] == algorithm
81+
else
82+
algorithm.valid_alg?(header['alg'])
83+
end
84+
end
85+
end
86+
87+
def verify_signature!
88+
return if valid_algorithms.any? { |algorithm| verify_signature_for?(algorithm, key) }
89+
90+
raise JWT::VerificationError, 'Signature verification failed'
91+
end
92+
93+
def verify_signature_for?(algorithm, key)
94+
if algorithm.is_a?(String)
95+
raise JWT::DecodeError, 'No verification key available' unless key
96+
97+
Array(key).any? { |k| Signature.verify(algorithm, k, signing_input, signature) }
98+
else
99+
algorithm.verify(signing_input, signature, key: key, header: header, payload: payload)
100+
end
101+
end
102+
103+
def verify_claims!
104+
Verify.verify_claims(payload, options)
105+
Verify.verify_required_claims(payload, options)
106+
end
107+
108+
def validate_segment_count!
109+
segment_count = segments.size
110+
111+
return if segment_count == 3
112+
return if segment_count == 2 && (!verify? || header['alg'] == 'none')
113+
114+
raise JWT::DecodeError, 'Not enough or too many segments'
115+
end
116+
117+
def decode_header(raw_header)
118+
decode_segment_default(raw_header)
119+
end
120+
121+
def decode_payload(raw_segment)
122+
if @options[:decode_payload_proc]
123+
@options[:decode_payload_proc].call(raw_segment, header, signature)
124+
else
125+
decode_segment_default(raw_segment)
126+
end
127+
end
128+
129+
def decode_segment_default(raw_segment)
130+
json_parse(Base64.urlsafe_decode64(raw_segment))
131+
rescue ArgumentError
132+
raise JWT::DecodeError, 'Invalid segment encoding'
133+
end
134+
135+
def json_parse(decoded_segment)
136+
JWT::JSON.parse(decoded_segment)
137+
rescue ::JSON::ParserError
138+
raise JWT::DecodeError, 'Invalid segment encoding'
139+
end
140+
end
141+
end

lib/jwt/encode.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ def initialize(options)
1414
@options = options
1515
@payload = options[:payload]
1616
@key = options[:key]
17-
_, @algorithm = Algos.find(options[:algorithm])
17+
18+
if (@algorithm_implementation = options[:algorithm_implementation]).nil?
19+
_, @algorithm = Algos.find(options[:algorithm])
20+
else
21+
@algorithm = @algorithm_implementation.alg
22+
end
23+
1824
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
1925
end
2026

@@ -58,7 +64,13 @@ def encode_payload
5864
def encode_signature
5965
return '' if @algorithm == ALG_NONE
6066

61-
Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false)
67+
Base64.urlsafe_encode64(signature, padding: false)
68+
end
69+
70+
def signature
71+
return @algorithm_implementation.sign(encoded_header_and_payload, key: @key) if @algorithm_implementation
72+
73+
JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key)
6274
end
6375

6476
def encode(data)

lib/jwt/extension/decode.rb

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,24 @@ def jwk_resolver(&block)
1818
@jwk_resolver
1919
end
2020

21-
def decode!(payload, options = {})
22-
::JWT::Decode.new(payload,
23-
decode_signing_key_from_options(options),
24-
true,
25-
create_decode_options(options)).decode_segments
21+
def decode!(token, options = {})
22+
Internals.decode!(token, options, self)
2623
end
2724

28-
def decode_signing_key_from_options(options)
29-
options[:signing_key] || self.signing_key
30-
end
31-
32-
def create_decode_options(given_options)
33-
::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(decode_payload_proc: self.decode_payload,
34-
algorithms: self.decoding_algorithms,
35-
jwks: self.jwk_resolver)
36-
.merge(given_options)
37-
end
25+
module Internals
26+
class << self
27+
def decode!(token, options, context)
28+
::JWT::DecodeToken.new(token, build_decode_options(options, context)).decoded_segments
29+
end
3830

39-
def decoding_algorithms
40-
(Array(self.algorithm) + Array(self.algorithms)).uniq
31+
def build_decode_options(options, context)
32+
::JWT::DefaultOptions::DEFAULT_OPTIONS.merge(key: options[:signing_key] || context.verification_key || context.signing_key,
33+
decode_payload_proc: context.decode_payload,
34+
algorithms: (Array(context.algorithm) + Array(context.algorithms)).uniq,
35+
jwks: context.jwk_resolver)
36+
.merge(options)
37+
end
38+
end
4139
end
4240
end
4341
end

lib/jwt/extension/encode.rb

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,33 @@ def encode_payload(&block)
1414
end
1515

1616
def encode!(payload, options = {})
17-
::JWT::Encode.new(
18-
payload: payload,
19-
key: signing_key_from_options(options),
20-
algorithm: self.algorithm,
21-
encode_payload_proc: self.encode_payload,
22-
headers: Array(options[:headers])
23-
).segments
17+
Internals.encode!(payload, options, self)
2418
end
2519

26-
def signing_key_from_options(options)
27-
key = options[:signing_key] || self.signing_key
28-
raise ::JWT::SigningKeyMissing, 'No key given for signing' if key.nil?
20+
module Internals
21+
class << self
22+
def encode!(payload, options, context)
23+
::JWT::Encode.new(build_options(payload, options, context)).segments
24+
end
2925

30-
key
26+
def build_options(payload, options, context)
27+
opts = {
28+
payload: payload,
29+
key: options[:key] || context.signing_key,
30+
encode_payload_proc: context.encode_payload,
31+
headers: Array(options[:headers])
32+
}
33+
34+
if (algo = context.algorithm).is_a?(String)
35+
opts[:algorithm] = algo
36+
raise ::JWT::SigningKeyMissing, 'No key given for signing' if opts[:key].nil?
37+
else
38+
opts[:algorithm_implementation] = algo
39+
end
40+
41+
opts
42+
end
43+
end
3144
end
3245
end
3346
end

lib/jwt/extension/keys.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ def signing_key(value = nil)
77
@signing_key = value unless value.nil?
88
@signing_key
99
end
10+
11+
def verification_key(value = nil)
12+
@verification_key = value unless value.nil?
13+
@verification_key
14+
end
1015
end
1116
end
1217
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe 'Custom Signing algorithm' do
4+
let(:payload) { { 'pay' => 'load'} }
5+
6+
let(:signing_algo) do
7+
Class.new do
8+
class << self
9+
def alg
10+
'CustomStatic'
11+
end
12+
13+
def valid_alg?(_alg)
14+
true
15+
end
16+
17+
def sign(_to_sign, _options)
18+
'static'
19+
end
20+
21+
def verify(_to_verify, signature, _options)
22+
signature == 'static'
23+
end
24+
end
25+
end
26+
end
27+
28+
subject(:extension) do
29+
algo = signing_algo
30+
31+
Class.new do
32+
include JWT
33+
algorithm algo
34+
end
35+
end
36+
37+
context 'when encoding' do
38+
it 'adds the custom signature to the end' do
39+
expect(::Base64.decode64(subject.encode!(payload).split('.')[2])).to eq('static')
40+
end
41+
end
42+
43+
context 'when decoding signed token' do
44+
let(:presigned_token) { subject.encode!(payload) }
45+
it 'verifies and decodes the payload' do
46+
expect(subject.decode!(presigned_token)).to eq([{'pay' => 'load'}, {'alg' => 'CustomStatic'}])
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)