jwt 2.8.2 → 2.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/README.md +189 -93
- data/lib/jwt/base64.rb +3 -0
- data/lib/jwt/claims/audience.rb +30 -0
- data/lib/jwt/claims/crit.rb +35 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +32 -0
- data/lib/jwt/claims/issued_at.rb +22 -0
- data/lib/jwt/claims/issuer.rb +34 -0
- data/lib/jwt/claims/jwt_id.rb +35 -0
- data/lib/jwt/claims/not_before.rb +32 -0
- data/lib/jwt/claims/numeric.rb +77 -0
- data/lib/jwt/claims/required.rb +33 -0
- data/lib/jwt/claims/subject.rb +30 -0
- data/lib/jwt/claims/verification_methods.rb +20 -0
- data/lib/jwt/claims/verifier.rb +61 -0
- data/lib/jwt/claims.rb +74 -0
- data/lib/jwt/claims_validator.rb +6 -25
- data/lib/jwt/configuration/container.rb +20 -0
- data/lib/jwt/configuration/decode_configuration.rb +24 -0
- data/lib/jwt/configuration/jwk_configuration.rb +1 -0
- data/lib/jwt/configuration.rb +8 -0
- data/lib/jwt/decode.rb +28 -70
- data/lib/jwt/deprecations.rb +1 -0
- data/lib/jwt/encode.rb +17 -60
- data/lib/jwt/encoded_token.rb +139 -0
- data/lib/jwt/error.rb +34 -0
- data/lib/jwt/json.rb +1 -1
- data/lib/jwt/jwa/compat.rb +32 -0
- data/lib/jwt/jwa/ecdsa.rb +39 -25
- data/lib/jwt/jwa/eddsa.rb +20 -27
- data/lib/jwt/jwa/hmac.rb +25 -18
- data/lib/jwt/jwa/hmac_rbnacl.rb +43 -43
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +40 -39
- data/lib/jwt/jwa/none.rb +8 -3
- data/lib/jwt/jwa/ps.rb +20 -15
- data/lib/jwt/jwa/rsa.rb +20 -10
- data/lib/jwt/jwa/signing_algorithm.rb +63 -0
- data/lib/jwt/jwa/unsupported.rb +9 -8
- data/lib/jwt/jwa/wrapper.rb +27 -9
- data/lib/jwt/jwa.rb +30 -34
- data/lib/jwt/jwk/ec.rb +2 -3
- data/lib/jwt/jwk/hmac.rb +2 -3
- data/lib/jwt/jwk/key_base.rb +1 -0
- data/lib/jwt/jwk/key_finder.rb +1 -0
- data/lib/jwt/jwk/kid_as_key_digest.rb +1 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +3 -4
- data/lib/jwt/jwk/rsa.rb +2 -3
- data/lib/jwt/jwk/set.rb +2 -0
- data/lib/jwt/jwk.rb +1 -0
- data/lib/jwt/token.rb +112 -0
- data/lib/jwt/verify.rb +16 -93
- data/lib/jwt/version.rb +30 -9
- data/lib/jwt.rb +20 -0
- data/ruby-jwt.gemspec +1 -0
- metadata +36 -7
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# JWT
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/jwt)
|
4
|
-
[](https://github.com/jwt/ruby-jwt/actions)
|
4
|
+
[](https://github.com/jwt/ruby-jwt/actions)
|
5
5
|
[](https://codeclimate.com/github/jwt/ruby-jwt)
|
6
6
|
[](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
|
7
7
|
[](https://codeclimate.com/github/jwt/ruby-jwt)
|
@@ -10,13 +10,30 @@ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools
|
|
10
10
|
|
11
11
|
If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
|
12
12
|
|
13
|
-
## Announcements
|
14
|
-
* Ruby 2.4 support was dropped in version 2.4.0
|
15
|
-
* Ruby 1.9.3 support was dropped at December 31st, 2016.
|
16
|
-
* Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133)
|
17
|
-
|
18
13
|
See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
|
19
14
|
|
15
|
+
## Upcoming breaking changes
|
16
|
+
|
17
|
+
Notable changes in the upcoming **version 3.0**:
|
18
|
+
|
19
|
+
- The indirect dependency to [rbnacl](https://github.com/RubyCrypto/rbnacl) will be removed:
|
20
|
+
- Support for the non-standard SHA512256 algorithm will be removed.
|
21
|
+
- Support for Ed25519 will be moved to a [separate gem](https://github.com/anakinj/jwt-eddsa) for better dependency handling.
|
22
|
+
|
23
|
+
- Base64 decoding will no longer fallback on the looser RFC 2045.
|
24
|
+
|
25
|
+
- Claim verification has been [split into separate classes](https://github.com/jwt/ruby-jwt/pull/605) and has [a new api](https://github.com/jwt/ruby-jwt/pull/626) and lead to the following deprecations:
|
26
|
+
- The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`.
|
27
|
+
- The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`.
|
28
|
+
- The `::JWT::JWA.create` method will be removed.
|
29
|
+
- The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`.
|
30
|
+
- Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
|
31
|
+
- Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
|
32
|
+
|
33
|
+
- The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes lead to a few deprecations and new requirements:
|
34
|
+
- The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed.
|
35
|
+
- Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module.
|
36
|
+
|
20
37
|
## Sponsors
|
21
38
|
|
22
39
|
|Logo|Message|
|
@@ -26,20 +43,32 @@ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
|
|
26
43
|
## Installing
|
27
44
|
|
28
45
|
### Using Rubygems:
|
46
|
+
|
29
47
|
```bash
|
30
48
|
gem install jwt
|
31
49
|
```
|
32
50
|
|
33
51
|
### Using Bundler:
|
52
|
+
|
34
53
|
Add the following to your Gemfile
|
35
54
|
```
|
36
55
|
gem 'jwt'
|
37
56
|
```
|
57
|
+
|
38
58
|
And run `bundle install`
|
39
59
|
|
60
|
+
Finally require the gem in your application
|
61
|
+
```ruby
|
62
|
+
require 'jwt'
|
63
|
+
```
|
64
|
+
|
40
65
|
## Algorithms and Usage
|
41
66
|
|
42
|
-
The
|
67
|
+
The jwt gem natively supports the NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms via the openssl library. The gem can be extended with additional or alternative implementations of the algorithms via extensions.
|
68
|
+
|
69
|
+
Additionally the EdDSA algorithm is supported via a [separate gem](https://rubygems.org/gems/jwt-eddsa).
|
70
|
+
|
71
|
+
For safe cryptographic signing, you need to specify the algorithm in the options hash whenever you call `JWT.decode` to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
|
43
72
|
|
44
73
|
See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
|
45
74
|
|
@@ -65,18 +94,17 @@ The stricter base64 decoding when processing tokens can be done via the `strict_
|
|
65
94
|
* none - unsigned token
|
66
95
|
|
67
96
|
```ruby
|
68
|
-
require 'jwt'
|
69
97
|
|
70
98
|
payload = { data: 'test' }
|
71
99
|
|
72
100
|
# IMPORTANT: set nil as password parameter
|
73
|
-
token = JWT.encode
|
101
|
+
token = JWT.encode(payload, nil, 'none')
|
74
102
|
|
75
103
|
# eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
|
76
104
|
puts token
|
77
105
|
|
78
106
|
# Set password to nil and validation to false otherwise this won't work
|
79
|
-
decoded_token = JWT.decode
|
107
|
+
decoded_token = JWT.decode(token, nil, false)
|
80
108
|
|
81
109
|
# Array
|
82
110
|
# [
|
@@ -96,12 +124,12 @@ puts decoded_token
|
|
96
124
|
# The secret must be a string. With OpenSSL 3.0/openssl gem `<3.0.1`, JWT::DecodeError will be raised if it isn't provided.
|
97
125
|
hmac_secret = 'my$ecretK3y'
|
98
126
|
|
99
|
-
token = JWT.encode
|
127
|
+
token = JWT.encode(payload, hmac_secret, 'HS256')
|
100
128
|
|
101
129
|
# eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
|
102
130
|
puts token
|
103
131
|
|
104
|
-
decoded_token = JWT.decode
|
132
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
|
105
133
|
|
106
134
|
# Array
|
107
135
|
# [
|
@@ -118,15 +146,15 @@ puts decoded_token
|
|
118
146
|
* RS512 - RSA using SHA-512 hash algorithm
|
119
147
|
|
120
148
|
```ruby
|
121
|
-
rsa_private = OpenSSL::PKey::RSA.generate
|
149
|
+
rsa_private = OpenSSL::PKey::RSA.generate(2048)
|
122
150
|
rsa_public = rsa_private.public_key
|
123
151
|
|
124
|
-
token = JWT.encode
|
152
|
+
token = JWT.encode(payload, rsa_private, 'RS256')
|
125
153
|
|
126
154
|
# eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
|
127
155
|
puts token
|
128
156
|
|
129
|
-
decoded_token = JWT.decode
|
157
|
+
decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' })
|
130
158
|
|
131
159
|
# Array
|
132
160
|
# [
|
@@ -146,12 +174,12 @@ puts decoded_token
|
|
146
174
|
```ruby
|
147
175
|
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
|
148
176
|
|
149
|
-
token = JWT.encode
|
177
|
+
token = JWT.encode(payload, ecdsa_key, 'ES256')
|
150
178
|
|
151
179
|
# eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
|
152
180
|
puts token
|
153
181
|
|
154
|
-
decoded_token = JWT.decode
|
182
|
+
decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' })
|
155
183
|
|
156
184
|
# Array
|
157
185
|
# [
|
@@ -206,12 +234,12 @@ gem 'openssl', '~> 2.1'
|
|
206
234
|
rsa_private = OpenSSL::PKey::RSA.generate 2048
|
207
235
|
rsa_public = rsa_private.public_key
|
208
236
|
|
209
|
-
token = JWT.encode
|
237
|
+
token = JWT.encode(payload, rsa_private, 'PS256')
|
210
238
|
|
211
239
|
# eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
|
212
240
|
puts token
|
213
241
|
|
214
|
-
decoded_token = JWT.decode
|
242
|
+
decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' })
|
215
243
|
|
216
244
|
# Array
|
217
245
|
# [
|
@@ -221,22 +249,57 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
|
|
221
249
|
puts decoded_token
|
222
250
|
```
|
223
251
|
|
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
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
token = JWT.encode(payload, key, algorithm='HS256', header_fields={})
|
258
|
+
```
|
259
|
+
|
260
|
+
**Example:**
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
|
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
|
281
|
+
```
|
282
|
+
|
224
283
|
### **Custom algorithms**
|
225
284
|
|
226
|
-
|
285
|
+
When encoding or decoding a token, you can pass in a custom object through the `algorithm` option to handle signing or verification. This custom object must include or extend the `JWT::JWA::SigningAlgorithm` module and implement certain methods:
|
286
|
+
|
287
|
+
- For decoding/verifying: The object must implement the methods `alg` and `verify`.
|
288
|
+
- For encoding/signing: The object must implement the methods `alg` and `sign`.
|
289
|
+
|
290
|
+
For customization options check the details from `JWT::JWA::SigningAlgorithm`.
|
291
|
+
|
227
292
|
|
228
293
|
```ruby
|
229
294
|
module CustomHS512Algorithm
|
295
|
+
extend JWT::JWA::SigningAlgorithm
|
296
|
+
|
230
297
|
def self.alg
|
231
298
|
'HS512'
|
232
299
|
end
|
233
300
|
|
234
|
-
def self.valid_alg?(alg_to_validate)
|
235
|
-
alg_to_validate == alg
|
236
|
-
end
|
237
|
-
|
238
301
|
def self.sign(data:, signing_key:)
|
239
|
-
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'),
|
302
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data)
|
240
303
|
end
|
241
304
|
|
242
305
|
def self.verify(data:, signature:, verification_key:)
|
@@ -248,49 +311,63 @@ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
|
|
248
311
|
payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
|
249
312
|
```
|
250
313
|
|
251
|
-
##
|
252
|
-
JSON Web Token defines some reserved claim names and defines how they should be
|
253
|
-
used. JWT supports these reserved claim names:
|
314
|
+
## `JWT::Token` and `JWT::EncodedToken`
|
254
315
|
|
255
|
-
|
256
|
-
- 'nbf' (Not Before Time) Claim
|
257
|
-
- 'iss' (Issuer) Claim
|
258
|
-
- 'aud' (Audience) Claim
|
259
|
-
- 'jti' (JWT ID) Claim
|
260
|
-
- 'iat' (Issued At) Claim
|
261
|
-
- 'sub' (Subject) Claim
|
316
|
+
The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs.
|
262
317
|
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
+
```
|
266
324
|
|
325
|
+
The `JWT::EncodedToken` can be used to create a token object that allows verification of signatures and claims
|
267
326
|
```ruby
|
268
|
-
|
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'}
|
269
336
|
```
|
270
337
|
|
271
|
-
|
338
|
+
### Detached payload
|
339
|
+
|
340
|
+
The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT.
|
272
341
|
|
273
342
|
```ruby
|
274
|
-
|
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
|
+
```
|
275
349
|
|
276
|
-
payload
|
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.
|
277
351
|
|
278
|
-
|
279
|
-
|
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
|
+
```
|
280
358
|
|
281
|
-
|
282
|
-
puts token
|
359
|
+
## Claims
|
283
360
|
|
284
|
-
|
285
|
-
|
361
|
+
JSON Web Token defines some reserved claim names and defines how they should be
|
362
|
+
used. JWT supports these reserved claim names:
|
286
363
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
294
371
|
|
295
372
|
### Expiration Time Claim
|
296
373
|
|
@@ -304,10 +381,10 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
|
|
304
381
|
exp = Time.now.to_i + 4 * 3600
|
305
382
|
exp_payload = { data: 'data', exp: exp }
|
306
383
|
|
307
|
-
token = JWT.encode
|
384
|
+
token = JWT.encode(exp_payload, hmac_secret, 'HS256')
|
308
385
|
|
309
386
|
begin
|
310
|
-
decoded_token = JWT.decode
|
387
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
|
311
388
|
rescue JWT::ExpiredSignature
|
312
389
|
# Handle expired token, e.g. logout user or deny access
|
313
390
|
end
|
@@ -316,7 +393,7 @@ end
|
|
316
393
|
The Expiration Claim verification can be disabled.
|
317
394
|
```ruby
|
318
395
|
# Decode token without raising JWT::ExpiredSignature error
|
319
|
-
JWT.decode
|
396
|
+
JWT.decode(token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' })
|
320
397
|
```
|
321
398
|
|
322
399
|
**Adding Leeway**
|
@@ -328,11 +405,11 @@ leeway = 30 # seconds
|
|
328
405
|
exp_payload = { data: 'data', exp: exp }
|
329
406
|
|
330
407
|
# build expired token
|
331
|
-
token = JWT.encode
|
408
|
+
token = JWT.encode(exp_payload, hmac_secret, 'HS256')
|
332
409
|
|
333
410
|
begin
|
334
411
|
# add leeway to ensure the token is still accepted
|
335
|
-
decoded_token = JWT.decode
|
412
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' })
|
336
413
|
rescue JWT::ExpiredSignature
|
337
414
|
# Handle expired token, e.g. logout user or deny access
|
338
415
|
end
|
@@ -350,10 +427,10 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
|
|
350
427
|
nbf = Time.now.to_i - 3600
|
351
428
|
nbf_payload = { data: 'data', nbf: nbf }
|
352
429
|
|
353
|
-
token = JWT.encode
|
430
|
+
token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
|
354
431
|
|
355
432
|
begin
|
356
|
-
decoded_token = JWT.decode
|
433
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
|
357
434
|
rescue JWT::ImmatureSignature
|
358
435
|
# Handle invalid token, e.g. logout user or deny access
|
359
436
|
end
|
@@ -362,7 +439,7 @@ end
|
|
362
439
|
The Not Before Claim verification can be disabled.
|
363
440
|
```ruby
|
364
441
|
# Decode token without raising JWT::ImmatureSignature error
|
365
|
-
JWT.decode
|
442
|
+
JWT.decode(token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' })
|
366
443
|
```
|
367
444
|
|
368
445
|
**Adding Leeway**
|
@@ -374,11 +451,11 @@ leeway = 30
|
|
374
451
|
nbf_payload = { data: 'data', nbf: nbf }
|
375
452
|
|
376
453
|
# build expired token
|
377
|
-
token = JWT.encode
|
454
|
+
token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
|
378
455
|
|
379
456
|
begin
|
380
457
|
# add leeway to ensure the token is valid
|
381
|
-
decoded_token = JWT.decode
|
458
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' })
|
382
459
|
rescue JWT::ImmatureSignature
|
383
460
|
# Handle invalid token, e.g. logout user or deny access
|
384
461
|
end
|
@@ -396,11 +473,11 @@ You can pass multiple allowed issuers as an Array, verification will pass if one
|
|
396
473
|
iss = 'My Awesome Company Inc. or https://my.awesome.website/'
|
397
474
|
iss_payload = { data: 'data', iss: iss }
|
398
475
|
|
399
|
-
token = JWT.encode
|
476
|
+
token = JWT.encode(iss_payload, hmac_secret, 'HS256')
|
400
477
|
|
401
478
|
begin
|
402
479
|
# Add iss to the validation to check if the token has been manipulated
|
403
|
-
decoded_token = JWT.decode
|
480
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' })
|
404
481
|
rescue JWT::InvalidIssuerError
|
405
482
|
# Handle invalid token, e.g. logout user or deny access
|
406
483
|
end
|
@@ -411,24 +488,24 @@ On supported ruby versions (>= 2.5) you can also delegate to methods, on older v
|
|
411
488
|
to convert them to proc (using `to_proc`)
|
412
489
|
|
413
490
|
```ruby
|
414
|
-
JWT.decode
|
491
|
+
JWT.decode(token, hmac_secret, true,
|
415
492
|
iss: %r'https://my.awesome.website/',
|
416
493
|
verify_iss: true,
|
417
|
-
algorithm: 'HS256'
|
494
|
+
algorithm: 'HS256')
|
418
495
|
```
|
419
496
|
|
420
497
|
```ruby
|
421
|
-
JWT.decode
|
498
|
+
JWT.decode(token, hmac_secret, true,
|
422
499
|
iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
|
423
500
|
verify_iss: true,
|
424
|
-
algorithm: 'HS256'
|
501
|
+
algorithm: 'HS256')
|
425
502
|
```
|
426
503
|
|
427
504
|
```ruby
|
428
|
-
JWT.decode
|
505
|
+
JWT.decode(token, hmac_secret, true,
|
429
506
|
iss: method(:valid_issuer?),
|
430
507
|
verify_iss: true,
|
431
|
-
algorithm: 'HS256'
|
508
|
+
algorithm: 'HS256')
|
432
509
|
|
433
510
|
# somewhere in the same class:
|
434
511
|
def valid_issuer?(issuer)
|
@@ -446,11 +523,11 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
|
|
446
523
|
aud = ['Young', 'Old']
|
447
524
|
aud_payload = { data: 'data', aud: aud }
|
448
525
|
|
449
|
-
token = JWT.encode
|
526
|
+
token = JWT.encode(aud_payload, hmac_secret, 'HS256')
|
450
527
|
|
451
528
|
begin
|
452
529
|
# Add aud to the validation to check if the token has been manipulated
|
453
|
-
decoded_token = JWT.decode
|
530
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' })
|
454
531
|
rescue JWT::InvalidAudError
|
455
532
|
# Handle invalid token, e.g. logout user or deny access
|
456
533
|
puts 'Audience Error'
|
@@ -469,15 +546,15 @@ jti_raw = [hmac_secret, iat].join(':').to_s
|
|
469
546
|
jti = Digest::MD5.hexdigest(jti_raw)
|
470
547
|
jti_payload = { data: 'data', iat: iat, jti: jti }
|
471
548
|
|
472
|
-
token = JWT.encode
|
549
|
+
token = JWT.encode(jti_payload, hmac_secret, 'HS256')
|
473
550
|
|
474
551
|
begin
|
475
552
|
# If :verify_jti is true, validation will pass if a JTI is present
|
476
|
-
#decoded_token = JWT.decode
|
553
|
+
#decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' })
|
477
554
|
# Alternatively, pass a proc with your own code to check if the JTI has already been used
|
478
|
-
decoded_token = JWT.decode
|
555
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' })
|
479
556
|
# or
|
480
|
-
decoded_token = JWT.decode
|
557
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' })
|
481
558
|
rescue JWT::InvalidJtiError
|
482
559
|
# Handle invalid token, e.g. logout user or deny access
|
483
560
|
puts 'Error'
|
@@ -496,11 +573,11 @@ From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.or
|
|
496
573
|
iat = Time.now.to_i
|
497
574
|
iat_payload = { data: 'data', iat: iat }
|
498
575
|
|
499
|
-
token = JWT.encode
|
576
|
+
token = JWT.encode(iat_payload, hmac_secret, 'HS256')
|
500
577
|
|
501
578
|
begin
|
502
579
|
# Add iat to the validation to check if the token has been manipulated
|
503
|
-
decoded_token = JWT.decode
|
580
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' })
|
504
581
|
rescue JWT::InvalidIatError
|
505
582
|
# Handle invalid token, e.g. logout user or deny access
|
506
583
|
end
|
@@ -516,16 +593,32 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
|
|
516
593
|
sub = 'Subject'
|
517
594
|
sub_payload = { data: 'data', sub: sub }
|
518
595
|
|
519
|
-
token = JWT.encode
|
596
|
+
token = JWT.encode(sub_payload, hmac_secret, 'HS256')
|
520
597
|
|
521
598
|
begin
|
522
599
|
# Add sub to the validation to check if the token has been manipulated
|
523
|
-
decoded_token = JWT.decode
|
600
|
+
decoded_token = JWT.decode(token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' })
|
524
601
|
rescue JWT::InvalidSubError
|
525
602
|
# Handle invalid token, e.g. logout user or deny access
|
526
603
|
end
|
527
604
|
```
|
528
605
|
|
606
|
+
### Standalone claim verification
|
607
|
+
|
608
|
+
The JWT claim verifications can be used to verify any Hash to include expected keys and values.
|
609
|
+
|
610
|
+
A few example on verifying the claims for a payload:
|
611
|
+
```ruby
|
612
|
+
JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :numeric, :exp)
|
613
|
+
JWT::Claims.valid_payload?({"exp" => Time.now.to_i + 10}, :exp)
|
614
|
+
# => true
|
615
|
+
JWT::Claims.payload_errors({"exp" => Time.now.to_i - 10}, :exp)
|
616
|
+
# => [#<struct JWT::Claims::Error message="Signature has expired">]
|
617
|
+
JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11})
|
618
|
+
|
619
|
+
JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10, "sub" => "subject"}, :exp, sub: "subject")
|
620
|
+
```
|
621
|
+
|
529
622
|
### Finding a Key
|
530
623
|
|
531
624
|
To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT.
|
@@ -536,7 +629,7 @@ iss_payload = { data: 'data', iss: issuers.first }
|
|
536
629
|
|
537
630
|
secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
|
538
631
|
|
539
|
-
token = JWT.encode
|
632
|
+
token = JWT.encode(iss_payload, hmac_secret, 'HS256')
|
540
633
|
|
541
634
|
begin
|
542
635
|
# Add iss to the validation to check if the token has been manipulated
|
@@ -553,7 +646,7 @@ end
|
|
553
646
|
You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
|
554
647
|
```ruby
|
555
648
|
# Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
|
556
|
-
JWT.decode
|
649
|
+
JWT.decode(token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' })
|
557
650
|
```
|
558
651
|
|
559
652
|
### X.509 certificates in x5c header
|
@@ -577,7 +670,7 @@ rescue JWT::DecodeError
|
|
577
670
|
end
|
578
671
|
```
|
579
672
|
|
580
|
-
|
673
|
+
## JSON Web Key (JWK)
|
581
674
|
|
582
675
|
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.
|
583
676
|
|
@@ -604,7 +697,6 @@ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
|
|
604
697
|
JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
|
605
698
|
```
|
606
699
|
|
607
|
-
|
608
700
|
The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
|
609
701
|
This can be used to implement caching of remotely fetched JWK Sets.
|
610
702
|
|
@@ -690,21 +782,25 @@ jwk_hash = jwk.export
|
|
690
782
|
thumbprint_as_the_kid = jwk_hash[:kid]
|
691
783
|
```
|
692
784
|
|
693
|
-
# Development and
|
785
|
+
# Development and testing
|
694
786
|
|
695
|
-
|
787
|
+
The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
|
696
788
|
|
697
789
|
```bash
|
698
|
-
|
790
|
+
bundle install
|
791
|
+
bundle exec appraisal rake test
|
699
792
|
```
|
700
793
|
|
701
|
-
|
794
|
+
# Releasing
|
795
|
+
|
796
|
+
To cut a new release adjust the [version.rb](lib/jwt/version.rb) and [CHANGELOG](CHANGELOG.md) with desired version numbers and dates and commit the changes. Tag the release with the version number using the following command:
|
702
797
|
|
703
798
|
```bash
|
704
|
-
|
705
|
-
bundle exec appraisal rake test
|
799
|
+
rake release:source_control_push
|
706
800
|
```
|
707
801
|
|
802
|
+
This will tag a new version an trigger a [GitHub action](.github/workflows/push_gem.yml) that eventually will push the gem to rubygems.org.
|
803
|
+
|
708
804
|
## How to contribute
|
709
805
|
See [CONTRIBUTING](CONTRIBUTING.md).
|
710
806
|
|
data/lib/jwt/base64.rb
CHANGED
@@ -4,15 +4,18 @@ require 'base64'
|
|
4
4
|
|
5
5
|
module JWT
|
6
6
|
# Base64 encoding and decoding
|
7
|
+
# @api private
|
7
8
|
class Base64
|
8
9
|
class << self
|
9
10
|
# Encode a string with URL-safe Base64 complying with RFC 4648 (not padded).
|
11
|
+
# @api private
|
10
12
|
def url_encode(str)
|
11
13
|
::Base64.urlsafe_encode64(str, padding: false)
|
12
14
|
end
|
13
15
|
|
14
16
|
# Decode a string with URL-safe Base64 complying with RFC 4648.
|
15
17
|
# Deprecated support for RFC 2045 remains for now. ("All line breaks or other characters not found in Table 1 must be ignored by decoding software")
|
18
|
+
# @api private
|
16
19
|
def url_decode(str)
|
17
20
|
::Base64.urlsafe_decode64(str)
|
18
21
|
rescue ArgumentError => e
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Audience class is responsible for validating the audience claim ('aud') in a JWT token.
|
6
|
+
class Audience
|
7
|
+
# Initializes a new Audience instance.
|
8
|
+
#
|
9
|
+
# @param expected_audience [String, Array<String>] the expected audience(s) for the JWT token.
|
10
|
+
def initialize(expected_audience:)
|
11
|
+
@expected_audience = expected_audience
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the audience claim ('aud') in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidAudError] if the audience claim is invalid.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
aud = context.payload['aud']
|
22
|
+
raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :expected_audience
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# Responsible of validation the crit header
|
6
|
+
class Crit
|
7
|
+
# Initializes a new Crit instance.
|
8
|
+
#
|
9
|
+
# @param expected_crits [String] the expected crit header values for the JWT token.
|
10
|
+
def initialize(expected_crits:)
|
11
|
+
@expected_crits = Array(expected_crits)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the critical claim ('crit') in the JWT token header.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload and header.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidCritError] if the crit claim is invalid.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit']
|
22
|
+
raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array)
|
23
|
+
|
24
|
+
missing = (expected_crits - context.header['crit'])
|
25
|
+
raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any?
|
26
|
+
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :expected_crits
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|