jwt 1.5.4 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.codeclimate.yml +6 -18
- data/.github/workflows/coverage.yml +27 -0
- data/.github/workflows/test.yml +67 -0
- data/.gitignore +7 -0
- data/.reek.yml +22 -0
- data/.rspec +1 -1
- data/.rubocop.yml +66 -1
- data/.sourcelevel.yml +17 -0
- data/AUTHORS +119 -0
- data/Appraisals +13 -0
- data/CHANGELOG.md +786 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/Gemfile +4 -1
- data/README.md +332 -79
- data/Rakefile +15 -0
- data/lib/jwt/algos/ecdsa.rb +64 -0
- data/lib/jwt/algos/eddsa.rb +35 -0
- data/lib/jwt/algos/hmac.rb +36 -0
- data/lib/jwt/algos/none.rb +17 -0
- data/lib/jwt/algos/ps.rb +43 -0
- data/lib/jwt/algos/rsa.rb +22 -0
- data/lib/jwt/algos/unsupported.rb +19 -0
- 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 +119 -30
- data/lib/jwt/encode.rb +69 -0
- data/lib/jwt/error.rb +10 -0
- data/lib/jwt/json.rb +11 -9
- 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 +59 -0
- data/lib/jwt/signature.rb +35 -0
- data/lib/jwt/verify.rb +59 -44
- data/lib/jwt/version.rb +8 -3
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +16 -162
- data/ruby-jwt.gemspec +14 -8
- metadata +71 -84
- data/.travis.yml +0 -13
- data/Manifest +0 -8
- 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/jwt/verify_spec.rb +0 -175
- data/spec/jwt_spec.rb +0 -232
- data/spec/spec_helper.rb +0 -31
data/README.md
CHANGED
@@ -1,24 +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://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=master)](https://github.com/jwt/ruby-jwt/actions)
|
4
5
|
[![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
|
5
6
|
[![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
|
6
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)
|
7
9
|
|
8
|
-
A
|
10
|
+
A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
|
9
11
|
|
10
|
-
If you have further questions
|
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).
|
11
13
|
|
12
14
|
## Announcements
|
15
|
+
* Ruby 2.4 support was dropped in version 2.4.0
|
16
|
+
* Ruby 1.9.3 support was dropped at December 31st, 2016.
|
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)
|
13
18
|
|
14
|
-
|
15
|
-
|
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)|
|
16
26
|
|
17
27
|
## Installing
|
18
28
|
|
19
29
|
### Using Rubygems:
|
20
30
|
```bash
|
21
|
-
|
31
|
+
gem install jwt
|
22
32
|
```
|
23
33
|
|
24
34
|
### Using Bundler:
|
@@ -30,23 +40,23 @@ And run `bundle install`
|
|
30
40
|
|
31
41
|
## Algorithms and Usage
|
32
42
|
|
33
|
-
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**
|
34
44
|
|
35
45
|
See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
|
36
46
|
|
37
|
-
**NONE**
|
47
|
+
### **NONE**
|
38
48
|
|
39
49
|
* none - unsigned token
|
40
50
|
|
41
51
|
```ruby
|
42
52
|
require 'jwt'
|
43
53
|
|
44
|
-
payload = {:
|
54
|
+
payload = { data: 'test' }
|
45
55
|
|
46
56
|
# IMPORTANT: set nil as password parameter
|
47
57
|
token = JWT.encode payload, nil, 'none'
|
48
58
|
|
49
|
-
#
|
59
|
+
# eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
|
50
60
|
puts token
|
51
61
|
|
52
62
|
# Set password to nil and validation to false otherwise this won't work
|
@@ -55,36 +65,44 @@ decoded_token = JWT.decode token, nil, false
|
|
55
65
|
# Array
|
56
66
|
# [
|
57
67
|
# {"data"=>"test"}, # payload
|
58
|
-
# {"
|
68
|
+
# {"alg"=>"none"} # header
|
59
69
|
# ]
|
60
70
|
puts decoded_token
|
61
71
|
```
|
62
72
|
|
63
|
-
**HMAC**
|
73
|
+
### **HMAC**
|
64
74
|
|
65
|
-
* HS256
|
66
|
-
*
|
75
|
+
* HS256 - HMAC using SHA-256 hash algorithm
|
76
|
+
* HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
|
77
|
+
* HS384 - HMAC using SHA-384 hash algorithm
|
67
78
|
* HS512 - HMAC using SHA-512 hash algorithm
|
68
79
|
|
69
80
|
```ruby
|
81
|
+
# The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
|
70
82
|
hmac_secret = 'my$ecretK3y'
|
71
83
|
|
72
84
|
token = JWT.encode payload, hmac_secret, 'HS256'
|
73
85
|
|
74
|
-
#
|
86
|
+
# eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
|
75
87
|
puts token
|
76
88
|
|
77
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
89
|
+
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
78
90
|
|
79
91
|
# Array
|
80
92
|
# [
|
81
93
|
# {"data"=>"test"}, # payload
|
82
|
-
# {"
|
94
|
+
# {"alg"=>"HS256"} # header
|
83
95
|
# ]
|
84
96
|
puts decoded_token
|
85
97
|
```
|
86
98
|
|
87
|
-
|
99
|
+
Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512-256, and HMAC-SHA512. RbNaCl enforces a maximum key size of 32 bytes for these algorithms.
|
100
|
+
|
101
|
+
[RbNaCl](https://github.com/cryptosphere/rbnacl) requires
|
102
|
+
[libsodium](https://github.com/jedisct1/libsodium), it can be installed
|
103
|
+
on MacOS with `brew install libsodium`.
|
104
|
+
|
105
|
+
### **RSA**
|
88
106
|
|
89
107
|
* RS256 - RSA using SHA-256 hash algorithm
|
90
108
|
* RS384 - RSA using SHA-384 hash algorithm
|
@@ -96,49 +114,103 @@ rsa_public = rsa_private.public_key
|
|
96
114
|
|
97
115
|
token = JWT.encode payload, rsa_private, 'RS256'
|
98
116
|
|
99
|
-
#
|
117
|
+
# eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
|
100
118
|
puts token
|
101
119
|
|
102
|
-
decoded_token = JWT.decode token, rsa_public, true, { :
|
120
|
+
decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
|
103
121
|
|
104
122
|
# Array
|
105
123
|
# [
|
106
124
|
# {"data"=>"test"}, # payload
|
107
|
-
# {"
|
125
|
+
# {"alg"=>"RS256"} # header
|
108
126
|
# ]
|
109
127
|
puts decoded_token
|
110
128
|
```
|
111
129
|
|
112
|
-
**ECDSA**
|
130
|
+
### **ECDSA**
|
113
131
|
|
114
132
|
* ES256 - ECDSA using P-256 and SHA-256
|
115
133
|
* ES384 - ECDSA using P-384 and SHA-384
|
116
134
|
* ES512 - ECDSA using P-521 and SHA-512
|
135
|
+
* ES256K - ECDSA using P-256K and SHA-256
|
117
136
|
|
118
137
|
```ruby
|
119
|
-
ecdsa_key = OpenSSL::PKey::EC.
|
120
|
-
ecdsa_key.generate_key
|
121
|
-
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
|
122
|
-
ecdsa_public.private_key = nil
|
138
|
+
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
|
123
139
|
|
124
140
|
token = JWT.encode payload, ecdsa_key, 'ES256'
|
125
141
|
|
126
|
-
#
|
142
|
+
# eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
|
127
143
|
puts token
|
128
144
|
|
129
|
-
decoded_token = JWT.decode token,
|
145
|
+
decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
|
130
146
|
|
131
147
|
# Array
|
132
148
|
# [
|
133
149
|
# {"test"=>"data"}, # payload
|
134
|
-
# {"
|
150
|
+
# {"alg"=>"ES256"} # header
|
135
151
|
# ]
|
136
152
|
puts decoded_token
|
137
153
|
```
|
138
154
|
|
139
|
-
**
|
155
|
+
### **EDDSA**
|
156
|
+
|
157
|
+
In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
gem 'rbnacl'
|
161
|
+
```
|
162
|
+
|
163
|
+
For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
|
164
|
+
|
165
|
+
* ED25519
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
private_key = RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF')
|
169
|
+
public_key = private_key.verify_key
|
170
|
+
token = JWT.encode payload, private_key, 'ED25519'
|
171
|
+
|
172
|
+
# eyJhbGciOiJFRDI1NTE5In0.eyJkYXRhIjoidGVzdCJ9.6xIztXyOupskddGA_RvKU76V9b2dCQUYhoZEVFnRimJoPYIzZ2Fm47CWw8k2NTCNpgfAuxg9OXjaiVK7MvrbCQ
|
173
|
+
puts token
|
174
|
+
|
175
|
+
decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
|
176
|
+
# Array
|
177
|
+
# [
|
178
|
+
# {"test"=>"data"}, # payload
|
179
|
+
# {"alg"=>"ED25519"} # header
|
180
|
+
# ]
|
181
|
+
|
182
|
+
```
|
183
|
+
|
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' }
|
140
206
|
|
141
|
-
|
207
|
+
# Array
|
208
|
+
# [
|
209
|
+
# {"data"=>"test"}, # payload
|
210
|
+
# {"alg"=>"PS256"} # header
|
211
|
+
# ]
|
212
|
+
puts decoded_token
|
213
|
+
```
|
142
214
|
|
143
215
|
## Support for reserved claim names
|
144
216
|
JSON Web Token defines some reserved claim names and defines how they should be
|
@@ -152,6 +224,38 @@ used. JWT supports these reserved claim names:
|
|
152
224
|
- 'iat' (Issued At) Claim
|
153
225
|
- 'sub' (Subject) Claim
|
154
226
|
|
227
|
+
## Add custom header fields
|
228
|
+
Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
|
229
|
+
To add custom header fields you need to pass `header_fields` parameter
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
token = JWT.encode payload, key, algorithm='HS256', header_fields={}
|
233
|
+
```
|
234
|
+
|
235
|
+
**Example:**
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
require 'jwt'
|
239
|
+
|
240
|
+
payload = { data: 'test' }
|
241
|
+
|
242
|
+
# IMPORTANT: set nil as password parameter
|
243
|
+
token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
|
244
|
+
|
245
|
+
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
|
246
|
+
puts token
|
247
|
+
|
248
|
+
# Set password to nil and validation to false otherwise this won't work
|
249
|
+
decoded_token = JWT.decode token, nil, false
|
250
|
+
|
251
|
+
# Array
|
252
|
+
# [
|
253
|
+
# {"data"=>"test"}, # payload
|
254
|
+
# {"typ"=>"JWT", "alg"=>"none"} # header
|
255
|
+
# ]
|
256
|
+
puts decoded_token
|
257
|
+
```
|
258
|
+
|
155
259
|
### Expiration Time Claim
|
156
260
|
|
157
261
|
From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4):
|
@@ -162,31 +266,37 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
|
|
162
266
|
|
163
267
|
```ruby
|
164
268
|
exp = Time.now.to_i + 4 * 3600
|
165
|
-
exp_payload = { :
|
269
|
+
exp_payload = { data: 'data', exp: exp }
|
166
270
|
|
167
271
|
token = JWT.encode exp_payload, hmac_secret, 'HS256'
|
168
272
|
|
169
273
|
begin
|
170
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
274
|
+
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
171
275
|
rescue JWT::ExpiredSignature
|
172
276
|
# Handle expired token, e.g. logout user or deny access
|
173
277
|
end
|
174
278
|
```
|
175
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
|
+
|
176
286
|
**Adding Leeway**
|
177
287
|
|
178
288
|
```ruby
|
179
289
|
exp = Time.now.to_i - 10
|
180
290
|
leeway = 30 # seconds
|
181
291
|
|
182
|
-
exp_payload = { :
|
292
|
+
exp_payload = { data: 'data', exp: exp }
|
183
293
|
|
184
294
|
# build expired token
|
185
295
|
token = JWT.encode exp_payload, hmac_secret, 'HS256'
|
186
296
|
|
187
297
|
begin
|
188
298
|
# add leeway to ensure the token is still accepted
|
189
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
299
|
+
decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
|
190
300
|
rescue JWT::ExpiredSignature
|
191
301
|
# Handle expired token, e.g. logout user or deny access
|
192
302
|
end
|
@@ -202,31 +312,37 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
|
|
202
312
|
|
203
313
|
```ruby
|
204
314
|
nbf = Time.now.to_i - 3600
|
205
|
-
nbf_payload = { :
|
315
|
+
nbf_payload = { data: 'data', nbf: nbf }
|
206
316
|
|
207
317
|
token = JWT.encode nbf_payload, hmac_secret, 'HS256'
|
208
318
|
|
209
319
|
begin
|
210
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
320
|
+
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
211
321
|
rescue JWT::ImmatureSignature
|
212
322
|
# Handle invalid token, e.g. logout user or deny access
|
213
323
|
end
|
214
324
|
```
|
215
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
|
+
|
216
332
|
**Adding Leeway**
|
217
333
|
|
218
334
|
```ruby
|
219
335
|
nbf = Time.now.to_i + 10
|
220
336
|
leeway = 30
|
221
337
|
|
222
|
-
nbf_payload = { :
|
338
|
+
nbf_payload = { data: 'data', nbf: nbf }
|
223
339
|
|
224
340
|
# build expired token
|
225
341
|
token = JWT.encode nbf_payload, hmac_secret, 'HS256'
|
226
342
|
|
227
343
|
begin
|
228
344
|
# add leeway to ensure the token is valid
|
229
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
345
|
+
decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
|
230
346
|
rescue JWT::ImmatureSignature
|
231
347
|
# Handle invalid token, e.g. logout user or deny access
|
232
348
|
end
|
@@ -238,20 +354,52 @@ From [Oauth JSON Web Token 4.1.1. "iss" (Issuer) Claim](https://tools.ietf.org/h
|
|
238
354
|
|
239
355
|
> The `iss` (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The `iss` value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL.
|
240
356
|
|
357
|
+
You can pass multiple allowed issuers as an Array, verification will pass if one of them matches the `iss` value in the payload.
|
358
|
+
|
241
359
|
```ruby
|
242
360
|
iss = 'My Awesome Company Inc. or https://my.awesome.website/'
|
243
|
-
iss_payload = { :
|
361
|
+
iss_payload = { data: 'data', iss: iss }
|
244
362
|
|
245
363
|
token = JWT.encode iss_payload, hmac_secret, 'HS256'
|
246
364
|
|
247
365
|
begin
|
248
366
|
# Add iss to the validation to check if the token has been manipulated
|
249
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
367
|
+
decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
|
250
368
|
rescue JWT::InvalidIssuerError
|
251
369
|
# Handle invalid token, e.g. logout user or deny access
|
252
370
|
end
|
253
371
|
```
|
254
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
|
+
|
255
403
|
### Audience Claim
|
256
404
|
|
257
405
|
From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
|
@@ -260,13 +408,13 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
|
|
260
408
|
|
261
409
|
```ruby
|
262
410
|
aud = ['Young', 'Old']
|
263
|
-
aud_payload = { :
|
411
|
+
aud_payload = { data: 'data', aud: aud }
|
264
412
|
|
265
413
|
token = JWT.encode aud_payload, hmac_secret, 'HS256'
|
266
414
|
|
267
415
|
begin
|
268
416
|
# Add aud to the validation to check if the token has been manipulated
|
269
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
417
|
+
decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
|
270
418
|
rescue JWT::InvalidAudError
|
271
419
|
# Handle invalid token, e.g. logout user or deny access
|
272
420
|
puts 'Audience Error'
|
@@ -283,37 +431,40 @@ From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/h
|
|
283
431
|
# Use the secret and iat to create a unique key per request to prevent replay attacks
|
284
432
|
jti_raw = [hmac_secret, iat].join(':').to_s
|
285
433
|
jti = Digest::MD5.hexdigest(jti_raw)
|
286
|
-
jti_payload = { :
|
434
|
+
jti_payload = { data: 'data', iat: iat, jti: jti }
|
287
435
|
|
288
436
|
token = JWT.encode jti_payload, hmac_secret, 'HS256'
|
289
437
|
|
290
438
|
begin
|
291
439
|
# If :verify_jti is true, validation will pass if a JTI is present
|
292
|
-
#decoded_token = JWT.decode token, hmac_secret, true, { :
|
440
|
+
#decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
|
293
441
|
# Alternatively, pass a proc with your own code to check if the JTI has already been used
|
294
|
-
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' }
|
295
445
|
rescue JWT::InvalidJtiError
|
296
446
|
# Handle invalid token, e.g. logout user or deny access
|
297
447
|
puts 'Error'
|
298
448
|
end
|
299
|
-
|
300
449
|
```
|
301
450
|
|
302
451
|
### Issued At Claim
|
303
452
|
|
304
453
|
From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):
|
305
454
|
|
306
|
-
> 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.
|
456
|
+
|
457
|
+
**Handle Issued At Claim**
|
307
458
|
|
308
459
|
```ruby
|
309
460
|
iat = Time.now.to_i
|
310
|
-
iat_payload = { :
|
461
|
+
iat_payload = { data: 'data', iat: iat }
|
311
462
|
|
312
463
|
token = JWT.encode iat_payload, hmac_secret, 'HS256'
|
313
464
|
|
314
465
|
begin
|
315
466
|
# Add iat to the validation to check if the token has been manipulated
|
316
|
-
decoded_token = JWT.decode token, hmac_secret, true, { :
|
467
|
+
decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
|
317
468
|
rescue JWT::InvalidIatError
|
318
469
|
# Handle invalid token, e.g. logout user or deny access
|
319
470
|
end
|
@@ -327,18 +478,140 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
|
|
327
478
|
|
328
479
|
```ruby
|
329
480
|
sub = 'Subject'
|
330
|
-
sub_payload = { :
|
481
|
+
sub_payload = { data: 'data', sub: sub }
|
331
482
|
|
332
483
|
token = JWT.encode sub_payload, hmac_secret, 'HS256'
|
333
484
|
|
334
485
|
begin
|
335
486
|
# Add sub to the validation to check if the token has been manipulated
|
336
|
-
decoded_token = JWT.decode token, hmac_secret, true, {
|
487
|
+
decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
|
337
488
|
rescue JWT::InvalidSubError
|
338
489
|
# Handle invalid token, e.g. logout user or deny access
|
339
490
|
end
|
340
491
|
```
|
341
492
|
|
493
|
+
### Finding a Key
|
494
|
+
|
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.
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
|
499
|
+
iss_payload = { data: 'data', iss: issuers.first }
|
500
|
+
|
501
|
+
secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
|
502
|
+
|
503
|
+
token = JWT.encode iss_payload, hmac_secret, 'HS256'
|
504
|
+
|
505
|
+
begin
|
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
|
511
|
+
# Handle invalid token, e.g. logout user or deny access
|
512
|
+
end
|
513
|
+
```
|
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
|
+
|
342
615
|
# Development and Tests
|
343
616
|
|
344
617
|
We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
|
@@ -347,40 +620,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
|
|
347
620
|
rake release
|
348
621
|
```
|
349
622
|
|
350
|
-
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.
|
351
624
|
|
352
625
|
```bash
|
353
|
-
bundle
|
626
|
+
bundle install
|
627
|
+
bundle exec appraisal rake test
|
354
628
|
```
|
355
629
|
|
356
|
-
|
630
|
+
## How to contribute
|
631
|
+
See [CONTRIBUTING](CONTRIBUTING.md).
|
357
632
|
|
358
633
|
## Contributors
|
359
634
|
|
360
|
-
|
361
|
-
* Ilya Zhitomirskiy <ilya@joindiaspora.com>
|
362
|
-
* Daniel Grippi <daniel@joindiaspora.com>
|
363
|
-
* Jeff Lindsay <progrium@gmail.com>
|
364
|
-
* Bob Aman <bob@sporkmonger.com>
|
365
|
-
* Micah Gates <github@mgates.com>
|
366
|
-
* Rob Wygand <rob@wygand.com>
|
367
|
-
* Ariel Salomon (Oscil8)
|
368
|
-
* Paul Battley <pbattley@gmail.com>
|
369
|
-
* Zane Shannon [@zshannon](https://github.com/zshannon)
|
370
|
-
* Brian Fletcher [@punkle](https://github.com/punkle)
|
371
|
-
* Alex [@ZhangHanDong](https://github.com/ZhangHanDong)
|
372
|
-
* John Downey [@jtdowney](https://github.com/jtdowney)
|
373
|
-
* Adam Greene [@skippy](https://github.com/skippy)
|
374
|
-
* Tim Rudat [@excpt](https://github.com/excpt) <timrudat@gmail.com> - Maintainer
|
635
|
+
See [AUTHORS](AUTHORS).
|
375
636
|
|
376
637
|
## License
|
377
638
|
|
378
|
-
|
379
|
-
|
380
|
-
Copyright (c) 2011 Jeff Lindsay
|
381
|
-
|
382
|
-
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:
|
383
|
-
|
384
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
385
|
-
|
386
|
-
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).
|