jwt 2.1.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +6 -18
- data/.github/workflows/coverage.yml +27 -0
- data/.github/workflows/test.yml +67 -0
- data/.gitignore +3 -1
- data/.reek.yml +21 -39
- data/.rspec +1 -0
- data/.rubocop.yml +21 -52
- data/{.ebert.yml → .sourcelevel.yml} +3 -4
- data/AUTHORS +119 -0
- data/Appraisals +13 -0
- data/CHANGELOG.md +329 -19
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/Gemfile +4 -0
- data/README.md +261 -100
- data/Rakefile +6 -1
- data/lib/jwt/algos/ecdsa.rb +37 -8
- data/lib/jwt/algos/eddsa.rb +16 -4
- data/lib/jwt/algos/hmac.rb +3 -0
- data/lib/jwt/algos/none.rb +17 -0
- data/lib/jwt/algos/ps.rb +43 -0
- data/lib/jwt/algos/rsa.rb +4 -1
- data/lib/jwt/algos/unsupported.rb +7 -4
- data/lib/jwt/algos.rb +44 -0
- data/lib/jwt/base64.rb +19 -0
- data/lib/jwt/claims_validator.rb +37 -0
- data/lib/jwt/configuration/container.rb +21 -0
- data/lib/jwt/configuration/decode_configuration.rb +46 -0
- data/lib/jwt/configuration/jwk_configuration.rb +27 -0
- data/lib/jwt/configuration.rb +15 -0
- data/lib/jwt/decode.rb +120 -24
- data/lib/jwt/encode.rb +43 -25
- data/lib/jwt/error.rb +6 -0
- data/lib/jwt/json.rb +18 -0
- data/lib/jwt/jwk/ec.rb +199 -0
- data/lib/jwt/jwk/hmac.rb +67 -0
- data/lib/jwt/jwk/key_base.rb +35 -0
- data/lib/jwt/jwk/key_finder.rb +62 -0
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/rsa.rb +138 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +52 -0
- data/lib/jwt/security_utils.rb +8 -0
- data/lib/jwt/signature.rb +7 -22
- data/lib/jwt/verify.rb +19 -8
- data/lib/jwt/version.rb +6 -2
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +12 -44
- data/ruby-jwt.gemspec +13 -9
- metadata +44 -97
- data/.travis.yml +0 -14
- data/Manifest +0 -8
- data/lib/jwt/default_options.rb +0 -15
- data/spec/fixtures/certs/ec256-private.pem +0 -8
- data/spec/fixtures/certs/ec256-public.pem +0 -4
- data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
- data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
- data/spec/fixtures/certs/ec384-private.pem +0 -9
- data/spec/fixtures/certs/ec384-public.pem +0 -5
- data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
- data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
- data/spec/fixtures/certs/ec512-private.pem +0 -10
- data/spec/fixtures/certs/ec512-public.pem +0 -6
- data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
- data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
- data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
- data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
- data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
- data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
- data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
- data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
- data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
- data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
- data/spec/integration/readme_examples_spec.rb +0 -202
- data/spec/jwt/verify_spec.rb +0 -232
- data/spec/jwt_spec.rb +0 -315
- data/spec/spec_helper.rb +0 -28
data/README.md
CHANGED
@@ -1,25 +1,34 @@
|
|
1
1
|
# JWT
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/jwt.svg)](https://badge.fury.io/rb/jwt)
|
4
|
-
[![Build Status](https://
|
4
|
+
[![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=master)](https://github.com/jwt/ruby-jwt/actions)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
|
6
6
|
[![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
|
7
7
|
[![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
|
8
|
+
[![SourceLevel](https://app.sourcelevel.io/github/jwt/-/ruby-jwt.svg)](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
|
8
9
|
|
9
|
-
A
|
10
|
+
A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
|
10
11
|
|
11
12
|
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
13
|
|
13
14
|
## Announcements
|
14
|
-
|
15
|
+
* Ruby 2.4 support was dropped in version 2.4.0
|
15
16
|
* Ruby 1.9.3 support was dropped at December 31st, 2016.
|
16
17
|
* 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
|
|
19
|
+
See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
|
20
|
+
|
21
|
+
## Sponsors
|
22
|
+
|
23
|
+
|Logo|Message|
|
24
|
+
|-|-|
|
25
|
+
|![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png)|If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth)|
|
26
|
+
|
18
27
|
## Installing
|
19
28
|
|
20
29
|
### Using Rubygems:
|
21
30
|
```bash
|
22
|
-
|
31
|
+
gem install jwt
|
23
32
|
```
|
24
33
|
|
25
34
|
### Using Bundler:
|
@@ -31,23 +40,23 @@ And run `bundle install`
|
|
31
40
|
|
32
41
|
## Algorithms and Usage
|
33
42
|
|
34
|
-
The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using 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/
|
43
|
+
The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using 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**
|
35
44
|
|
36
45
|
See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
|
37
46
|
|
38
|
-
**NONE**
|
47
|
+
### **NONE**
|
39
48
|
|
40
49
|
* none - unsigned token
|
41
50
|
|
42
51
|
```ruby
|
43
52
|
require 'jwt'
|
44
53
|
|
45
|
-
payload = {:
|
54
|
+
payload = { data: 'test' }
|
46
55
|
|
47
56
|
# IMPORTANT: set nil as password parameter
|
48
57
|
token = JWT.encode payload, nil, 'none'
|
49
58
|
|
50
|
-
#
|
59
|
+
# eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
|
51
60
|
puts token
|
52
61
|
|
53
62
|
# Set password to nil and validation to false otherwise this won't work
|
@@ -61,7 +70,7 @@ decoded_token = JWT.decode token, nil, false
|
|
61
70
|
puts decoded_token
|
62
71
|
```
|
63
72
|
|
64
|
-
**HMAC**
|
73
|
+
### **HMAC**
|
65
74
|
|
66
75
|
* HS256 - HMAC using SHA-256 hash algorithm
|
67
76
|
* HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
|
@@ -69,14 +78,15 @@ puts decoded_token
|
|
69
78
|
* HS512 - HMAC using SHA-512 hash algorithm
|
70
79
|
|
71
80
|
```ruby
|
81
|
+
# The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
|
72
82
|
hmac_secret = 'my$ecretK3y'
|
73
83
|
|
74
84
|
token = JWT.encode payload, hmac_secret, 'HS256'
|
75
85
|
|
76
|
-
#
|
86
|
+
# eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
|
77
87
|
puts token
|
78
88
|
|
79
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
89
|
+
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
80
90
|
|
81
91
|
# Array
|
82
92
|
# [
|
@@ -92,7 +102,7 @@ Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt
|
|
92
102
|
[libsodium](https://github.com/jedisct1/libsodium), it can be installed
|
93
103
|
on MacOS with `brew install libsodium`.
|
94
104
|
|
95
|
-
**RSA**
|
105
|
+
### **RSA**
|
96
106
|
|
97
107
|
* RS256 - RSA using SHA-256 hash algorithm
|
98
108
|
* RS384 - RSA using SHA-384 hash algorithm
|
@@ -104,10 +114,10 @@ rsa_public = rsa_private.public_key
|
|
104
114
|
|
105
115
|
token = JWT.encode payload, rsa_private, 'RS256'
|
106
116
|
|
107
|
-
#
|
117
|
+
# eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
|
108
118
|
puts token
|
109
119
|
|
110
|
-
decoded_token = JWT.decode token, rsa_public, true, { :
|
120
|
+
decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
|
111
121
|
|
112
122
|
# Array
|
113
123
|
# [
|
@@ -117,24 +127,22 @@ decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
|
|
117
127
|
puts decoded_token
|
118
128
|
```
|
119
129
|
|
120
|
-
**ECDSA**
|
130
|
+
### **ECDSA**
|
121
131
|
|
122
132
|
* ES256 - ECDSA using P-256 and SHA-256
|
123
133
|
* ES384 - ECDSA using P-384 and SHA-384
|
124
134
|
* ES512 - ECDSA using P-521 and SHA-512
|
135
|
+
* ES256K - ECDSA using P-256K and SHA-256
|
125
136
|
|
126
137
|
```ruby
|
127
|
-
ecdsa_key = OpenSSL::PKey::EC.
|
128
|
-
ecdsa_key.generate_key
|
129
|
-
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
|
130
|
-
ecdsa_public.private_key = nil
|
138
|
+
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
|
131
139
|
|
132
140
|
token = JWT.encode payload, ecdsa_key, 'ES256'
|
133
141
|
|
134
|
-
#
|
142
|
+
# eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
|
135
143
|
puts token
|
136
144
|
|
137
|
-
decoded_token = JWT.decode token,
|
145
|
+
decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
|
138
146
|
|
139
147
|
# Array
|
140
148
|
# [
|
@@ -144,7 +152,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { :algorithm => 'ES256' }
|
|
144
152
|
puts decoded_token
|
145
153
|
```
|
146
154
|
|
147
|
-
**EDDSA**
|
155
|
+
### **EDDSA**
|
148
156
|
|
149
157
|
In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
|
150
158
|
|
@@ -154,17 +162,17 @@ gem 'rbnacl'
|
|
154
162
|
|
155
163
|
For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
|
156
164
|
|
157
|
-
* ED25519
|
165
|
+
* ED25519
|
158
166
|
|
159
|
-
```ruby
|
160
|
-
private_key = RbNaCl::Signatures::Ed25519::SigningKey.new(
|
167
|
+
```ruby
|
168
|
+
private_key = RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF')
|
161
169
|
public_key = private_key.verify_key
|
162
|
-
token = JWT.encode payload, private_key, 'ED25519'
|
170
|
+
token = JWT.encode payload, private_key, 'ED25519'
|
163
171
|
|
164
|
-
# eyJhbGciOiJFRDI1NTE5In0.
|
172
|
+
# eyJhbGciOiJFRDI1NTE5In0.eyJkYXRhIjoidGVzdCJ9.6xIztXyOupskddGA_RvKU76V9b2dCQUYhoZEVFnRimJoPYIzZ2Fm47CWw8k2NTCNpgfAuxg9OXjaiVK7MvrbCQ
|
165
173
|
puts token
|
166
174
|
|
167
|
-
decoded_token = JWT.decode token, public_key, true, {:
|
175
|
+
decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
|
168
176
|
# Array
|
169
177
|
# [
|
170
178
|
# {"test"=>"data"}, # payload
|
@@ -173,9 +181,36 @@ decoded_token = JWT.decode token, public_key, true, {:algorithm => 'ED25519' }
|
|
173
181
|
|
174
182
|
```
|
175
183
|
|
176
|
-
**RSASSA-PSS**
|
184
|
+
### **RSASSA-PSS**
|
185
|
+
|
186
|
+
In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`.
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
gem 'openssl', '~> 2.1'
|
190
|
+
```
|
191
|
+
|
192
|
+
* PS256 - RSASSA-PSS using SHA-256 hash algorithm
|
193
|
+
* PS384 - RSASSA-PSS using SHA-384 hash algorithm
|
194
|
+
* PS512 - RSASSA-PSS using SHA-512 hash algorithm
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
rsa_private = OpenSSL::PKey::RSA.generate 2048
|
198
|
+
rsa_public = rsa_private.public_key
|
199
|
+
|
200
|
+
token = JWT.encode payload, rsa_private, 'PS256'
|
201
|
+
|
202
|
+
# eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
|
203
|
+
puts token
|
204
|
+
|
205
|
+
decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
|
177
206
|
|
178
|
-
|
207
|
+
# Array
|
208
|
+
# [
|
209
|
+
# {"data"=>"test"}, # payload
|
210
|
+
# {"alg"=>"PS256"} # header
|
211
|
+
# ]
|
212
|
+
puts decoded_token
|
213
|
+
```
|
179
214
|
|
180
215
|
## Support for reserved claim names
|
181
216
|
JSON Web Token defines some reserved claim names and defines how they should be
|
@@ -190,7 +225,7 @@ used. JWT supports these reserved claim names:
|
|
190
225
|
- 'sub' (Subject) Claim
|
191
226
|
|
192
227
|
## Add custom header fields
|
193
|
-
Ruby-jwt gem supports custom [header fields]
|
228
|
+
Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
|
194
229
|
To add custom header fields you need to pass `header_fields` parameter
|
195
230
|
|
196
231
|
```ruby
|
@@ -202,12 +237,12 @@ token = JWT.encode payload, key, algorithm='HS256', header_fields={}
|
|
202
237
|
```ruby
|
203
238
|
require 'jwt'
|
204
239
|
|
205
|
-
payload = {:
|
240
|
+
payload = { data: 'test' }
|
206
241
|
|
207
242
|
# IMPORTANT: set nil as password parameter
|
208
|
-
token = JWT.encode payload, nil, 'none', { :
|
243
|
+
token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
|
209
244
|
|
210
|
-
#
|
245
|
+
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
|
211
246
|
puts token
|
212
247
|
|
213
248
|
# Set password to nil and validation to false otherwise this won't work
|
@@ -231,31 +266,37 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
|
|
231
266
|
|
232
267
|
```ruby
|
233
268
|
exp = Time.now.to_i + 4 * 3600
|
234
|
-
exp_payload = { :
|
269
|
+
exp_payload = { data: 'data', exp: exp }
|
235
270
|
|
236
271
|
token = JWT.encode exp_payload, hmac_secret, 'HS256'
|
237
272
|
|
238
273
|
begin
|
239
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
274
|
+
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
240
275
|
rescue JWT::ExpiredSignature
|
241
276
|
# Handle expired token, e.g. logout user or deny access
|
242
277
|
end
|
243
278
|
```
|
244
279
|
|
280
|
+
The Expiration Claim verification can be disabled.
|
281
|
+
```ruby
|
282
|
+
# Decode token without raising JWT::ExpiredSignature error
|
283
|
+
JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
|
284
|
+
```
|
285
|
+
|
245
286
|
**Adding Leeway**
|
246
287
|
|
247
288
|
```ruby
|
248
289
|
exp = Time.now.to_i - 10
|
249
290
|
leeway = 30 # seconds
|
250
291
|
|
251
|
-
exp_payload = { :
|
292
|
+
exp_payload = { data: 'data', exp: exp }
|
252
293
|
|
253
294
|
# build expired token
|
254
295
|
token = JWT.encode exp_payload, hmac_secret, 'HS256'
|
255
296
|
|
256
297
|
begin
|
257
298
|
# add leeway to ensure the token is still accepted
|
258
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
299
|
+
decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
|
259
300
|
rescue JWT::ExpiredSignature
|
260
301
|
# Handle expired token, e.g. logout user or deny access
|
261
302
|
end
|
@@ -271,31 +312,37 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
|
|
271
312
|
|
272
313
|
```ruby
|
273
314
|
nbf = Time.now.to_i - 3600
|
274
|
-
nbf_payload = { :
|
315
|
+
nbf_payload = { data: 'data', nbf: nbf }
|
275
316
|
|
276
317
|
token = JWT.encode nbf_payload, hmac_secret, 'HS256'
|
277
318
|
|
278
319
|
begin
|
279
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
320
|
+
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
280
321
|
rescue JWT::ImmatureSignature
|
281
322
|
# Handle invalid token, e.g. logout user or deny access
|
282
323
|
end
|
283
324
|
```
|
284
325
|
|
326
|
+
The Not Before Claim verification can be disabled.
|
327
|
+
```ruby
|
328
|
+
# Decode token without raising JWT::ImmatureSignature error
|
329
|
+
JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
|
330
|
+
```
|
331
|
+
|
285
332
|
**Adding Leeway**
|
286
333
|
|
287
334
|
```ruby
|
288
335
|
nbf = Time.now.to_i + 10
|
289
336
|
leeway = 30
|
290
337
|
|
291
|
-
nbf_payload = { :
|
338
|
+
nbf_payload = { data: 'data', nbf: nbf }
|
292
339
|
|
293
340
|
# build expired token
|
294
341
|
token = JWT.encode nbf_payload, hmac_secret, 'HS256'
|
295
342
|
|
296
343
|
begin
|
297
344
|
# add leeway to ensure the token is valid
|
298
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
345
|
+
decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
|
299
346
|
rescue JWT::ImmatureSignature
|
300
347
|
# Handle invalid token, e.g. logout user or deny access
|
301
348
|
end
|
@@ -311,18 +358,48 @@ You can pass multiple allowed issuers as an Array, verification will pass if one
|
|
311
358
|
|
312
359
|
```ruby
|
313
360
|
iss = 'My Awesome Company Inc. or https://my.awesome.website/'
|
314
|
-
iss_payload = { :
|
361
|
+
iss_payload = { data: 'data', iss: iss }
|
315
362
|
|
316
363
|
token = JWT.encode iss_payload, hmac_secret, 'HS256'
|
317
364
|
|
318
365
|
begin
|
319
366
|
# Add iss to the validation to check if the token has been manipulated
|
320
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
367
|
+
decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
|
321
368
|
rescue JWT::InvalidIssuerError
|
322
369
|
# Handle invalid token, e.g. logout user or deny access
|
323
370
|
end
|
324
371
|
```
|
325
372
|
|
373
|
+
You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
|
374
|
+
On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
|
375
|
+
to convert them to proc (using `to_proc`)
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
JWT.decode token, hmac_secret, true,
|
379
|
+
iss: %r'https://my.awesome.website/',
|
380
|
+
verify_iss: true,
|
381
|
+
algorithm: 'HS256'
|
382
|
+
```
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
JWT.decode token, hmac_secret, true,
|
386
|
+
iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
|
387
|
+
verify_iss: true,
|
388
|
+
algorithm: 'HS256'
|
389
|
+
```
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
JWT.decode token, hmac_secret, true,
|
393
|
+
iss: method(:valid_issuer?),
|
394
|
+
verify_iss: true,
|
395
|
+
algorithm: 'HS256'
|
396
|
+
|
397
|
+
# somewhere in the same class:
|
398
|
+
def valid_issuer?(issuer)
|
399
|
+
# custom validation
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
326
403
|
### Audience Claim
|
327
404
|
|
328
405
|
From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
|
@@ -331,13 +408,13 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
|
|
331
408
|
|
332
409
|
```ruby
|
333
410
|
aud = ['Young', 'Old']
|
334
|
-
aud_payload = { :
|
411
|
+
aud_payload = { data: 'data', aud: aud }
|
335
412
|
|
336
413
|
token = JWT.encode aud_payload, hmac_secret, 'HS256'
|
337
414
|
|
338
415
|
begin
|
339
416
|
# Add aud to the validation to check if the token has been manipulated
|
340
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
417
|
+
decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
|
341
418
|
rescue JWT::InvalidAudError
|
342
419
|
# Handle invalid token, e.g. logout user or deny access
|
343
420
|
puts 'Audience Error'
|
@@ -354,83 +431,187 @@ From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/h
|
|
354
431
|
# Use the secret and iat to create a unique key per request to prevent replay attacks
|
355
432
|
jti_raw = [hmac_secret, iat].join(':').to_s
|
356
433
|
jti = Digest::MD5.hexdigest(jti_raw)
|
357
|
-
jti_payload = { :
|
434
|
+
jti_payload = { data: 'data', iat: iat, jti: jti }
|
358
435
|
|
359
436
|
token = JWT.encode jti_payload, hmac_secret, 'HS256'
|
360
437
|
|
361
438
|
begin
|
362
439
|
# If :verify_jti is true, validation will pass if a JTI is present
|
363
|
-
#decoded_token = JWT.decode token, hmac_secret, true, { :
|
440
|
+
#decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
|
364
441
|
# Alternatively, pass a proc with your own code to check if the JTI has already been used
|
365
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
442
|
+
decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
|
443
|
+
# or
|
444
|
+
decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
|
366
445
|
rescue JWT::InvalidJtiError
|
367
446
|
# Handle invalid token, e.g. logout user or deny access
|
368
447
|
puts 'Error'
|
369
448
|
end
|
370
|
-
|
371
449
|
```
|
372
450
|
|
373
451
|
### Issued At Claim
|
374
452
|
|
375
453
|
From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):
|
376
454
|
|
377
|
-
> The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL.
|
455
|
+
> The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. The `leeway` option is not taken into account when verifying this claim. The `iat_leeway` option was removed in version 2.2.0. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL.
|
378
456
|
|
379
457
|
**Handle Issued At Claim**
|
380
458
|
|
381
459
|
```ruby
|
382
460
|
iat = Time.now.to_i
|
383
|
-
iat_payload = { :
|
461
|
+
iat_payload = { data: 'data', iat: iat }
|
384
462
|
|
385
463
|
token = JWT.encode iat_payload, hmac_secret, 'HS256'
|
386
464
|
|
387
465
|
begin
|
388
466
|
# Add iat to the validation to check if the token has been manipulated
|
389
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
467
|
+
decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
|
390
468
|
rescue JWT::InvalidIatError
|
391
469
|
# Handle invalid token, e.g. logout user or deny access
|
392
470
|
end
|
393
471
|
```
|
394
472
|
|
395
|
-
|
473
|
+
### Subject Claim
|
396
474
|
|
397
|
-
|
398
|
-
iat = Time.now.to_i + 10
|
399
|
-
leeway = 30 # seconds
|
475
|
+
From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2):
|
400
476
|
|
401
|
-
|
477
|
+
> The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL.
|
402
478
|
|
403
|
-
|
404
|
-
|
479
|
+
```ruby
|
480
|
+
sub = 'Subject'
|
481
|
+
sub_payload = { data: 'data', sub: sub }
|
482
|
+
|
483
|
+
token = JWT.encode sub_payload, hmac_secret, 'HS256'
|
405
484
|
|
406
485
|
begin
|
407
|
-
#
|
408
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
409
|
-
rescue JWT::
|
486
|
+
# Add sub to the validation to check if the token has been manipulated
|
487
|
+
decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
|
488
|
+
rescue JWT::InvalidSubError
|
410
489
|
# Handle invalid token, e.g. logout user or deny access
|
411
490
|
end
|
412
491
|
```
|
413
492
|
|
414
|
-
###
|
493
|
+
### Finding a Key
|
415
494
|
|
416
|
-
|
417
|
-
|
418
|
-
> The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL.
|
495
|
+
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.
|
419
496
|
|
420
497
|
```ruby
|
421
|
-
|
422
|
-
|
498
|
+
issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
|
499
|
+
iss_payload = { data: 'data', iss: issuers.first }
|
423
500
|
|
424
|
-
|
501
|
+
secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
|
502
|
+
|
503
|
+
token = JWT.encode iss_payload, hmac_secret, 'HS256'
|
425
504
|
|
426
505
|
begin
|
427
|
-
# Add
|
428
|
-
decoded_token = JWT.decode
|
429
|
-
|
506
|
+
# Add iss to the validation to check if the token has been manipulated
|
507
|
+
decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
|
508
|
+
secrets[payload['iss']]
|
509
|
+
end
|
510
|
+
rescue JWT::InvalidIssuerError
|
430
511
|
# Handle invalid token, e.g. logout user or deny access
|
431
512
|
end
|
432
513
|
```
|
433
514
|
|
515
|
+
### Required Claims
|
516
|
+
|
517
|
+
You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
|
518
|
+
```ruby
|
519
|
+
# Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
|
520
|
+
JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
|
521
|
+
```
|
522
|
+
|
523
|
+
### X.509 certificates in x5c header
|
524
|
+
|
525
|
+
A JWT signature can be verified using certificate(s) given in the `x5c` header. Before doing that, the trustworthiness of these certificate(s) must be established. This is done in accordance with RFC 5280 which (among other things) verifies the certificate(s) are issued by a trusted root certificate, the timestamps are valid, and none of the certificate(s) are revoked (i.e. being present in the root certificate's Certificate Revocation List).
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
|
529
|
+
crl_uris = root_certificates.map(&:crl_uris)
|
530
|
+
crls = crl_uris.map do |uri|
|
531
|
+
# look up cached CRL by `uri` and return it if found, otherwise continue
|
532
|
+
crl = Net::HTTP.get(uri)
|
533
|
+
crl = OpenSSL::X509::CRL.new(crl)
|
534
|
+
# cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
|
535
|
+
end
|
536
|
+
|
537
|
+
begin
|
538
|
+
JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
|
539
|
+
rescue JWT::DecodeError
|
540
|
+
# Handle error, e.g. x5c header certificate revoked or expired
|
541
|
+
end
|
542
|
+
```
|
543
|
+
|
544
|
+
### JSON Web Key (JWK)
|
545
|
+
|
546
|
+
JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. The `jwks` option can be given as a lambda that evaluates every time a kid is resolved.
|
547
|
+
|
548
|
+
If the kid is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
|
549
|
+
|
550
|
+
```ruby
|
551
|
+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid')
|
552
|
+
payload = { data: 'data' }
|
553
|
+
headers = { kid: jwk.kid }
|
554
|
+
|
555
|
+
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
|
556
|
+
|
557
|
+
# The jwk loader would fetch the set of JWKs from a trusted source,
|
558
|
+
# to avoid malicious requests triggering cache invalidations there needs to be some kind of grace time or other logic for determining the validity of the invalidation.
|
559
|
+
# This example only allows cache invalidations every 5 minutes.
|
560
|
+
jwk_loader = ->(options) do
|
561
|
+
if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
|
562
|
+
logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
|
563
|
+
@cached_keys = nil
|
564
|
+
end
|
565
|
+
@cached_keys ||= begin
|
566
|
+
@cache_last_update = Time.now.to_i
|
567
|
+
{ keys: [jwk.export] }
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
begin
|
572
|
+
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
|
573
|
+
rescue JWT::JWKError
|
574
|
+
# Handle problems with the provided JWKs
|
575
|
+
rescue JWT::DecodeError
|
576
|
+
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
|
577
|
+
end
|
578
|
+
```
|
579
|
+
|
580
|
+
or by passing the JWKs as a simple Hash
|
581
|
+
|
582
|
+
```
|
583
|
+
jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
|
584
|
+
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
|
585
|
+
```
|
586
|
+
|
587
|
+
### Importing and exporting JSON Web Keys
|
588
|
+
|
589
|
+
The ::JWT::JWK class can be used to import and export both the public key (default behaviour) and the private key. To include the private key in the export pass the `include_private` parameter to the export method.
|
590
|
+
|
591
|
+
```ruby
|
592
|
+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
|
593
|
+
|
594
|
+
jwk_hash = jwk.export
|
595
|
+
jwk_hash_with_private_key = jwk.export(include_private: true)
|
596
|
+
```
|
597
|
+
|
598
|
+
### Key ID (kid) and JWKs
|
599
|
+
|
600
|
+
The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization.
|
601
|
+
|
602
|
+
```ruby
|
603
|
+
JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
|
604
|
+
# OR
|
605
|
+
JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
|
606
|
+
# OR
|
607
|
+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint)
|
608
|
+
|
609
|
+
jwk_hash = jwk.export
|
610
|
+
|
611
|
+
thumbprint_as_the_kid = jwk_hash[:kid]
|
612
|
+
|
613
|
+
```
|
614
|
+
|
434
615
|
# Development and Tests
|
435
616
|
|
436
617
|
We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
|
@@ -439,40 +620,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
|
|
439
620
|
rake release
|
440
621
|
```
|
441
622
|
|
442
|
-
The tests are written with rspec.
|
623
|
+
The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
|
443
624
|
|
444
625
|
```bash
|
445
|
-
bundle
|
626
|
+
bundle install
|
627
|
+
bundle exec appraisal rake test
|
446
628
|
```
|
447
629
|
|
448
|
-
|
630
|
+
## How to contribute
|
631
|
+
See [CONTRIBUTING](CONTRIBUTING.md).
|
449
632
|
|
450
633
|
## Contributors
|
451
634
|
|
452
|
-
|
453
|
-
* Ilya Zhitomirskiy <ilya@joindiaspora.com>
|
454
|
-
* Daniel Grippi <daniel@joindiaspora.com>
|
455
|
-
* Jeff Lindsay <progrium@gmail.com>
|
456
|
-
* Bob Aman <bob@sporkmonger.com>
|
457
|
-
* Micah Gates <github@mgates.com>
|
458
|
-
* Rob Wygand <rob@wygand.com>
|
459
|
-
* Ariel Salomon (Oscil8)
|
460
|
-
* Paul Battley <pbattley@gmail.com>
|
461
|
-
* Zane Shannon [@zshannon](https://github.com/zshannon)
|
462
|
-
* Brian Fletcher [@punkle](https://github.com/punkle)
|
463
|
-
* Alex [@ZhangHanDong](https://github.com/ZhangHanDong)
|
464
|
-
* John Downey [@jtdowney](https://github.com/jtdowney)
|
465
|
-
* Adam Greene [@skippy](https://github.com/skippy)
|
466
|
-
* Tim Rudat [@excpt](https://github.com/excpt) <timrudat@gmail.com> - Maintainer
|
635
|
+
See [AUTHORS](AUTHORS).
|
467
636
|
|
468
637
|
## License
|
469
638
|
|
470
|
-
|
471
|
-
|
472
|
-
Copyright (c) 2011 Jeff Lindsay
|
473
|
-
|
474
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
475
|
-
|
476
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
477
|
-
|
478
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
639
|
+
See [LICENSE](LICENSE).
|
data/Rakefile
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
1
4
|
require 'bundler/gem_tasks'
|
2
5
|
|
3
6
|
begin
|
4
7
|
require 'rspec/core/rake_task'
|
8
|
+
require 'rubocop/rake_task'
|
5
9
|
|
6
10
|
RSpec::Core::RakeTask.new(:test)
|
11
|
+
RuboCop::RakeTask.new(:rubocop)
|
7
12
|
|
8
|
-
task default:
|
13
|
+
task default: %i[rubocop test]
|
9
14
|
rescue LoadError
|
10
15
|
puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
|
11
16
|
end
|