Skip to content

Commit d9a87bc

Browse files
committed
Detached payload support for token object
1 parent e180765 commit d9a87bc

File tree

6 files changed

+150
-53
lines changed

6 files changed

+150
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
**Features:**
88

99
- JWT::Token and JWT::EncodedToken for signing and verifying tokens [#621](https://github.com/jwt/ruby-jwt/pull/621) ([@anakinj](https://github.com/anakinj))
10+
- Detached payload support for JWT::Token and JWT::EncodedToken [#630](https://github.com/jwt/ruby-jwt/pull/630) ([@anakinj](https://github.com/anakinj))
1011
- Your contribution here
1112

1213
**Fixes and enhancements:**

README.md

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -249,28 +249,35 @@ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' })
249249
puts decoded_token
250250
```
251251

252-
### Using a Token object
253-
254-
The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs.
252+
### Add custom header fields
253+
Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
254+
To add custom header fields you need to pass `header_fields` parameter
255255

256256
```ruby
257-
token = JWT::Token.new(payload: { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" }, header: { kid: 'hmac' })
258-
token.sign!(algorithm: 'HS256', key: "secret")
259-
260-
token.jwt # => "eyJhbGciOiJIUzI1N..."
257+
token = JWT.encode(payload, key, algorithm='HS256', header_fields={})
261258
```
262259

263-
The `JWT::EncodedToken` can be used to create a token object that allows verification of signatures and claims
260+
**Example:**
261+
264262
```ruby
265-
encoded_token = JWT::EncodedToken.new(token.jwt)
266263

267-
encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
268-
encoded_token.verify_signature!(algorithm: 'HS256', key: "wrong_secret") # raises JWT::VerificationError
269-
encoded_token.verify_claims!(:exp, :jti)
270-
encoded_token.verify_claims!(sub: ["not-my-subject"]) # raises JWT::InvalidSubError
271-
encoded_token.claim_errors(sub: ["not-my-subject"]).map(&:message) # => ["Invalid subject. Expected [\"not-my-subject\"], received my-subject"]
272-
encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
273-
encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
264+
payload = { data: 'test' }
265+
266+
# IMPORTANT: set nil as password parameter
267+
token = JWT.encode(payload, nil, 'none', { typ: 'JWT' })
268+
269+
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
270+
puts token
271+
272+
# Set password to nil and validation to false otherwise this won't work
273+
decoded_token = JWT.decode(token, nil, false)
274+
275+
# Array
276+
# [
277+
# {"data"=>"test"}, # payload
278+
# {"typ"=>"JWT", "alg"=>"none"} # header
279+
# ]
280+
puts decoded_token
274281
```
275282

276283
### **Custom algorithms**
@@ -304,48 +311,63 @@ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
304311
payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
305312
```
306313

307-
## Support for reserved claim names
308-
JSON Web Token defines some reserved claim names and defines how they should be
309-
used. JWT supports these reserved claim names:
314+
## `JWT::Token` and `JWT::EncodedToken`
310315

311-
- 'exp' (Expiration Time) Claim
312-
- 'nbf' (Not Before Time) Claim
313-
- 'iss' (Issuer) Claim
314-
- 'aud' (Audience) Claim
315-
- 'jti' (JWT ID) Claim
316-
- 'iat' (Issued At) Claim
317-
- 'sub' (Subject) Claim
316+
The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs.
318317

319-
## Add custom header fields
320-
Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
321-
To add custom header fields you need to pass `header_fields` parameter
318+
```ruby
319+
token = JWT::Token.new(payload: { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" }, header: { kid: 'hmac' })
320+
token.sign!(algorithm: 'HS256', key: "secret")
321+
322+
token.jwt # => "eyJhbGciOiJIUzI1N..."
323+
```
322324

325+
The `JWT::EncodedToken` can be used to create a token object that allows verification of signatures and claims
323326
```ruby
324-
token = JWT.encode(payload, key, algorithm='HS256', header_fields={})
327+
encoded_token = JWT::EncodedToken.new(token.jwt)
328+
329+
encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
330+
encoded_token.verify_signature!(algorithm: 'HS256', key: "wrong_secret") # raises JWT::VerificationError
331+
encoded_token.verify_claims!(:exp, :jti)
332+
encoded_token.verify_claims!(sub: ["not-my-subject"]) # raises JWT::InvalidSubError
333+
encoded_token.claim_errors(sub: ["not-my-subject"]).map(&:message) # => ["Invalid subject. Expected [\"not-my-subject\"], received my-subject"]
334+
encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
335+
encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
325336
```
326337

327-
**Example:**
338+
### Detached payload
339+
340+
The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT.
328341

329342
```ruby
343+
token = JWT::Token.new(payload: { pay: 'load' })
344+
token.sign!(algorithm: 'HS256', key: "secret")
345+
token.detach_payload!
346+
token.jwt # => "eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0"
347+
token.encoded_payload # => "eyJwYXkiOiJsb2FkIn0"
348+
```
330349

331-
payload = { data: 'test' }
350+
The `JWT::EncodedToken` class can be used to decode a token with a detached payload by providing the payload to the token instance in separate.
332351

333-
# IMPORTANT: set nil as password parameter
334-
token = JWT.encode(payload, nil, 'none', { typ: 'JWT' })
352+
```ruby
353+
encoded_token = JWT::EncodedToken.new(token.jwt)
354+
encoded_token.encoded_payload = "eyJwYXkiOiJsb2FkIn0"
355+
encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
356+
encoded_token.payload # => {"pay"=>"load"}
357+
```
335358

336-
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
337-
puts token
359+
## Claims
338360

339-
# Set password to nil and validation to false otherwise this won't work
340-
decoded_token = JWT.decode(token, nil, false)
361+
JSON Web Token defines some reserved claim names and defines how they should be
362+
used. JWT supports these reserved claim names:
341363

342-
# Array
343-
# [
344-
# {"data"=>"test"}, # payload
345-
# {"typ"=>"JWT", "alg"=>"none"} # header
346-
# ]
347-
puts decoded_token
348-
```
364+
- 'exp' (Expiration Time) Claim
365+
- 'nbf' (Not Before Time) Claim
366+
- 'iss' (Issuer) Claim
367+
- 'aud' (Audience) Claim
368+
- 'jti' (JWT ID) Claim
369+
- 'iat' (Issued At) Claim
370+
- 'sub' (Subject) Claim
349371

350372
### Expiration Time Claim
351373

@@ -648,7 +670,7 @@ rescue JWT::DecodeError
648670
end
649671
```
650672

651-
### JSON Web Key (JWK)
673+
## JSON Web Key (JWK)
652674

653675
JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve.
654676

lib/jwt/encoded_token.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def initialize(jwt)
2727

2828
@jwt = jwt
2929
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
30-
@signing_input = [encoded_header, encoded_payload].join('.')
3130
end
3231

3332
# Returns the decoded signature of the JWT token.
@@ -58,18 +57,21 @@ def header
5857
#
5958
# @return [Hash] the payload.
6059
def payload
61-
@payload ||= parse_and_decode(encoded_payload)
60+
@payload ||= encoded_payload == '' ? raise(JWT::DecodeError, 'Encoded payload is empty') : parse_and_decode(encoded_payload)
6261
end
6362

64-
# Returns the encoded payload of the JWT token.
63+
# Sets or returns the encoded payload of the JWT token.
6564
#
6665
# @return [String] the encoded payload.
67-
attr_reader :encoded_payload
66+
# @param value [String] the encoded payload to set.
67+
attr_accessor :encoded_payload
6868

6969
# Returns the signing input of the JWT token.
7070
#
7171
# @return [String] the signing input.
72-
attr_reader :signing_input
72+
def signing_input
73+
[encoded_header, encoded_payload].join('.')
74+
end
7375

7476
# Verifies the signature of the JWT token.
7577
#

lib/jwt/token.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,15 @@ def signing_input
7676
# @return [String] the JWT token as a string.
7777
# @raise [JWT::EncodeError] if the token is not signed or other encoding issues
7878
def jwt
79-
@jwt ||= (@signature && [encoded_header, encoded_payload, encoded_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed')
79+
@jwt ||= (@signature && [encoded_header, @detached_payload ? '' : encoded_payload, encoded_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed')
80+
end
81+
82+
# Detaches the payload according to https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
83+
#
84+
def detach_payload!
85+
@detached_payload = true
86+
87+
nil
8088
end
8189

8290
# Signs the JWT token.
@@ -92,6 +100,8 @@ def sign!(algorithm:, key:)
92100
header.merge!(algo.header)
93101
@signature = algo.sign(data: signing_input, signing_key: key)
94102
end
103+
104+
nil
95105
end
96106

97107
# Returns the JWT token as a string.

spec/jwt/encoded_token_spec.rb

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,31 @@
33
RSpec.describe JWT::EncodedToken do
44
let(:payload) { { 'pay' => 'load' } }
55
let(:encoded_token) { JWT.encode(payload, 'secret', 'HS256') }
6-
6+
let(:detached_payload_token) do
7+
JWT::Token.new(payload: payload).tap do |t|
8+
t.detach_payload!
9+
t.sign!(algorithm: 'HS256', key: 'secret')
10+
end
11+
end
712
subject(:token) { described_class.new(encoded_token) }
813

914
describe '#payload' do
1015
it { expect(token.payload).to eq(payload) }
16+
17+
context 'when payload is detached' do
18+
let(:encoded_token) { detached_payload_token.jwt }
19+
20+
context 'when payload provided in separate' do
21+
before { token.encoded_payload = detached_payload_token.encoded_payload }
22+
it { expect(token.payload).to eq(payload) }
23+
end
24+
25+
context 'when payload is not provided' do
26+
it 'raises decode error' do
27+
expect { token.payload }.to raise_error(JWT::DecodeError, 'Encoded payload is empty')
28+
end
29+
end
30+
end
1131
end
1232

1333
describe '#header' do
@@ -41,6 +61,23 @@
4161
end
4262
end
4363

64+
context 'when payload is detached' do
65+
let(:encoded_token) { detached_payload_token.jwt }
66+
67+
context 'when payload provided in separate' do
68+
before { token.encoded_payload = detached_payload_token.encoded_payload }
69+
it 'does not raise' do
70+
expect(token.verify_signature!(algorithm: 'HS256', key: 'secret')).to eq(nil)
71+
end
72+
end
73+
74+
context 'when payload is not provided' do
75+
it 'raises VerificationError' do
76+
expect { token.verify_signature!(algorithm: 'HS256', key: 'secret') }.to raise_error(JWT::VerificationError, 'Signature verification failed')
77+
end
78+
end
79+
end
80+
4481
context 'when key_finder is given' do
4582
it 'uses key provided by keyfinder' do
4683
expect(token.verify_signature!(algorithm: 'HS256', key_finder: ->(_token) { 'secret' })).to eq(nil)
@@ -97,6 +134,21 @@
97134
expect { token.verify_claims!({ exp: { leeway: 1000 }, nbf: {} }, :exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
98135
end
99136
end
137+
138+
context 'when payload is detached' do
139+
let(:encoded_token) { detached_payload_token.jwt }
140+
context 'when payload provided in separate' do
141+
before { token.encoded_payload = detached_payload_token.encoded_payload }
142+
it 'raises claim verification error' do
143+
expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
144+
end
145+
end
146+
context 'when payload is not provided' do
147+
it 'raises decode error' do
148+
expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::DecodeError, 'Encoded payload is empty')
149+
end
150+
end
151+
end
100152
end
101153
end
102154

spec/jwt/token_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,14 @@
4242
end
4343
end
4444
end
45+
46+
describe '#detach_payload!' do
47+
context 'before token is signed' do
48+
it 'detaches the payload' do
49+
token.detach_payload!
50+
token.sign!(algorithm: 'HS256', key: 'secret')
51+
expect(token.jwt).to eq('eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0')
52+
end
53+
end
54+
end
4555
end

0 commit comments

Comments
 (0)