omniauth-auth0 2.0.0 → 3.1.0
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 +5 -5
- data/.circleci/config.yml +63 -0
- data/.devcontainer/devcontainer.json +18 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/ISSUE_TEMPLATE/config.yml +8 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +39 -0
- data/.github/ISSUE_TEMPLATE/report_a_bug.md +55 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +32 -0
- data/.github/stale.yml +20 -0
- data/.github/workflows/semgrep.yml +24 -0
- data/.gitignore +5 -2
- data/.semgrepignore +4 -0
- data/.shiprc +7 -0
- data/.snyk +9 -0
- data/CHANGELOG.md +212 -4
- data/CONTRIBUTING.md +71 -0
- data/EXAMPLES.md +167 -0
- data/Gemfile +17 -17
- data/Gemfile.lock +180 -0
- data/README.md +117 -92
- data/Rakefile +2 -2
- data/codecov.yml +22 -0
- data/lib/omniauth/auth0/errors.rb +11 -0
- data/lib/omniauth/auth0/jwt_validator.rb +278 -0
- data/lib/omniauth/auth0/telemetry.rb +36 -0
- data/lib/omniauth/strategies/auth0.rb +89 -21
- data/lib/omniauth-auth0/version.rb +1 -1
- data/lib/omniauth-auth0.rb +1 -1
- data/omniauth-auth0.gemspec +6 -7
- data/opslevel.yml +6 -0
- data/spec/omniauth/auth0/jwt_validator_spec.rb +729 -0
- data/spec/omniauth/auth0/telemetry_spec.rb +28 -0
- data/spec/omniauth/strategies/auth0_spec.rb +160 -18
- data/spec/resources/jwks.json +28 -0
- data/spec/spec_helper.rb +12 -7
- metadata +54 -16
- data/.travis.yml +0 -6
@@ -0,0 +1,729 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
require 'jwt'
|
4
|
+
|
5
|
+
describe OmniAuth::Auth0::JWTValidator do
|
6
|
+
#
|
7
|
+
# Reused data
|
8
|
+
#
|
9
|
+
|
10
|
+
let(:client_id) { 'CLIENT_ID' }
|
11
|
+
let(:client_secret) { 'CLIENT_SECRET' }
|
12
|
+
let(:domain) { 'samples.auth0.com' }
|
13
|
+
let(:future_timecode) { 32_503_680_000 }
|
14
|
+
let(:past_timecode) { 303_912_000 }
|
15
|
+
let(:valid_jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
|
16
|
+
|
17
|
+
let(:rsa_private_key) do
|
18
|
+
OpenSSL::PKey::RSA.generate 2048
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:valid_jwks) do
|
22
|
+
{
|
23
|
+
keys: [
|
24
|
+
{
|
25
|
+
kid: valid_jwks_kid,
|
26
|
+
x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)]
|
27
|
+
}
|
28
|
+
]
|
29
|
+
}.to_json
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:jwks) do
|
33
|
+
current_dir = File.dirname(__FILE__)
|
34
|
+
jwks_file = File.read("#{current_dir}/../../resources/jwks.json")
|
35
|
+
JSON.parse(jwks_file, symbolize_names: true)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Specs
|
40
|
+
#
|
41
|
+
|
42
|
+
describe 'JWT verifier default values' do
|
43
|
+
let(:jwt_validator) do
|
44
|
+
make_jwt_validator
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should have the correct issuer' do
|
48
|
+
expect(jwt_validator.issuer).to eq('https://samples.auth0.com/')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'JWT verifier token_head' do
|
53
|
+
let(:jwt_validator) do
|
54
|
+
make_jwt_validator
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should parse the head of a valid JWT' do
|
58
|
+
expect(jwt_validator.token_head(make_hs256_token)[:alg]).to eq('HS256')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should fail parsing the head of a blank JWT' do
|
62
|
+
expect(jwt_validator.token_head('')).to eq({})
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should fail parsing the head of an invalid JWT' do
|
66
|
+
expect(jwt_validator.token_head('.')).to eq({})
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should throw an exception for invalid JSON' do
|
70
|
+
expect do
|
71
|
+
jwt_validator.token_head('QXV0aDA=')
|
72
|
+
end.to raise_error(JSON::ParserError)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'JWT verifier jwks_public_cert' do
|
77
|
+
let(:jwt_validator) do
|
78
|
+
make_jwt_validator
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should return a public_key' do
|
82
|
+
x5c = jwks[:keys].first[:x5c].first
|
83
|
+
public_cert = jwt_validator.jwks_public_cert(x5c)
|
84
|
+
expect(public_cert.instance_of?(OpenSSL::PKey::RSA)).to eq(true)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should fail with an invalid x5c' do
|
88
|
+
expect do
|
89
|
+
jwt_validator.jwks_public_cert('QXV0aDA=')
|
90
|
+
end.to raise_error(OpenSSL::X509::CertificateError)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'JWT verifier jwks key parsing' do
|
95
|
+
let(:jwt_validator) do
|
96
|
+
make_jwt_validator
|
97
|
+
end
|
98
|
+
|
99
|
+
before do
|
100
|
+
stub_complete_jwks
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should return a key' do
|
104
|
+
expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256')
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should return an x5c key' do
|
108
|
+
expect(jwt_validator.jwks_key(:x5c, valid_jwks_kid).length).to eq(1)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should return nil if there is not key' do
|
112
|
+
expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should return nil if the key ID is invalid' do
|
116
|
+
expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe 'JWT verifier custom issuer' do
|
121
|
+
context 'same as domain' do
|
122
|
+
let(:jwt_validator) do
|
123
|
+
make_jwt_validator(opt_issuer: domain)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should have the correct issuer' do
|
127
|
+
expect(jwt_validator.issuer).to eq('https://samples.auth0.com/')
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should have the correct domain' do
|
131
|
+
expect(jwt_validator.issuer).to eq('https://samples.auth0.com/')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'different from domain' do
|
136
|
+
shared_examples_for 'has correct issuer and domain' do
|
137
|
+
let(:jwt_validator) { make_jwt_validator(opt_issuer: opt_issuer) }
|
138
|
+
|
139
|
+
it 'should have the correct issuer' do
|
140
|
+
expect(jwt_validator.issuer).to eq('https://different.auth0.com/')
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should have the correct domain' do
|
144
|
+
expect(jwt_validator.domain).to eq('https://samples.auth0.com/')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'without protocol and trailing slash' do
|
149
|
+
let(:opt_issuer) { 'different.auth0.com' }
|
150
|
+
it_behaves_like 'has correct issuer and domain'
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'with protocol and trailing slash' do
|
154
|
+
let(:opt_issuer) { 'https://different.auth0.com/' }
|
155
|
+
it_behaves_like 'has correct issuer and domain'
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'JWT verifier verify' do
|
161
|
+
let(:jwt_validator) do
|
162
|
+
make_jwt_validator
|
163
|
+
end
|
164
|
+
|
165
|
+
before do
|
166
|
+
stub_complete_jwks
|
167
|
+
stub_expected_jwks
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should fail when JWT is nil' do
|
171
|
+
expect do
|
172
|
+
jwt_validator.verify(nil)
|
173
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
174
|
+
message: "ID token is required but missing"
|
175
|
+
}))
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should fail when JWT is not well-formed' do
|
179
|
+
expect do
|
180
|
+
jwt_validator.verify('abc.123')
|
181
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
182
|
+
message: "ID token could not be decoded"
|
183
|
+
}))
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should fail with missing issuer' do
|
187
|
+
expect do
|
188
|
+
jwt_validator.verify(make_hs256_token)
|
189
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
190
|
+
message: "Issuer (iss) claim must be a string present in the ID token"
|
191
|
+
}))
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'should fail with invalid issuer' do
|
195
|
+
payload = {
|
196
|
+
iss: 'https://auth0.com/'
|
197
|
+
}
|
198
|
+
token = make_hs256_token(payload)
|
199
|
+
expect do
|
200
|
+
jwt_validator.verify(token)
|
201
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
202
|
+
message: "Issuer (iss) claim mismatch in the ID token, expected (https://samples.auth0.com/), found (https://auth0.com/)"
|
203
|
+
}))
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should fail when subject is missing' do
|
207
|
+
payload = {
|
208
|
+
iss: "https://#{domain}/",
|
209
|
+
sub: ''
|
210
|
+
}
|
211
|
+
token = make_hs256_token(payload)
|
212
|
+
expect do
|
213
|
+
jwt_validator.verify(token)
|
214
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
215
|
+
message: "Subject (sub) claim must be a string present in the ID token"
|
216
|
+
}))
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'should fail with missing audience' do
|
220
|
+
payload = {
|
221
|
+
iss: "https://#{domain}/",
|
222
|
+
sub: 'sub'
|
223
|
+
}
|
224
|
+
token = make_hs256_token(payload)
|
225
|
+
expect do
|
226
|
+
jwt_validator.verify(token)
|
227
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
228
|
+
message: "Audience (aud) claim must be a string or array of strings present in the ID token"
|
229
|
+
}))
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'should fail with invalid audience' do
|
233
|
+
payload = {
|
234
|
+
iss: "https://#{domain}/",
|
235
|
+
sub: 'sub',
|
236
|
+
aud: 'Auth0'
|
237
|
+
}
|
238
|
+
token = make_hs256_token(payload)
|
239
|
+
expect do
|
240
|
+
jwt_validator.verify(token)
|
241
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
242
|
+
message: "Audience (aud) claim mismatch in the ID token; expected #{client_id} but found Auth0"
|
243
|
+
}))
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'should fail when missing expiration' do
|
247
|
+
payload = {
|
248
|
+
iss: "https://#{domain}/",
|
249
|
+
sub: 'sub',
|
250
|
+
aud: client_id
|
251
|
+
}
|
252
|
+
|
253
|
+
token = make_hs256_token(payload)
|
254
|
+
expect do
|
255
|
+
jwt_validator.verify(token)
|
256
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
257
|
+
message: "Expiration time (exp) claim must be a number present in the ID token"
|
258
|
+
}))
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'should fail when past expiration' do
|
262
|
+
payload = {
|
263
|
+
iss: "https://#{domain}/",
|
264
|
+
sub: 'sub',
|
265
|
+
aud: client_id,
|
266
|
+
exp: past_timecode
|
267
|
+
}
|
268
|
+
|
269
|
+
token = make_hs256_token(payload)
|
270
|
+
expect do
|
271
|
+
jwt_validator.verify(token)
|
272
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
273
|
+
message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(past_timecode + 60)})"
|
274
|
+
}))
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'should pass when past expiration but within default leeway' do
|
278
|
+
exp = Time.now.to_i - 59
|
279
|
+
payload = {
|
280
|
+
iss: "https://#{domain}/",
|
281
|
+
sub: 'sub',
|
282
|
+
aud: client_id,
|
283
|
+
exp: exp,
|
284
|
+
iat: past_timecode
|
285
|
+
}
|
286
|
+
|
287
|
+
token = make_hs256_token(payload)
|
288
|
+
id_token = jwt_validator.verify(token)
|
289
|
+
expect(id_token['exp']).to eq(exp)
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'should fail when past expiration and outside default leeway' do
|
293
|
+
exp = Time.now.to_i - 61
|
294
|
+
payload = {
|
295
|
+
iss: "https://#{domain}/",
|
296
|
+
sub: 'sub',
|
297
|
+
aud: client_id,
|
298
|
+
exp: exp,
|
299
|
+
iat: past_timecode
|
300
|
+
}
|
301
|
+
|
302
|
+
token = make_hs256_token(payload)
|
303
|
+
expect do
|
304
|
+
jwt_validator.verify(token)
|
305
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
306
|
+
message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(exp + 60)})"
|
307
|
+
}))
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'should fail when missing iat' do
|
311
|
+
payload = {
|
312
|
+
iss: "https://#{domain}/",
|
313
|
+
sub: 'sub',
|
314
|
+
aud: client_id,
|
315
|
+
exp: future_timecode
|
316
|
+
}
|
317
|
+
|
318
|
+
token = make_hs256_token(payload)
|
319
|
+
expect do
|
320
|
+
jwt_validator.verify(token)
|
321
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
322
|
+
message: "Issued At (iat) claim must be a number present in the ID token"
|
323
|
+
}))
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'should fail when authorize params has nonce but nonce is missing in the token' do
|
327
|
+
payload = {
|
328
|
+
iss: "https://#{domain}/",
|
329
|
+
sub: 'sub',
|
330
|
+
aud: client_id,
|
331
|
+
exp: future_timecode,
|
332
|
+
iat: past_timecode
|
333
|
+
}
|
334
|
+
|
335
|
+
token = make_hs256_token(payload)
|
336
|
+
expect do
|
337
|
+
jwt_validator.verify(token, { nonce: 'noncey' })
|
338
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
339
|
+
message: "Nonce (nonce) claim must be a string present in the ID token"
|
340
|
+
}))
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'should fail when authorize params has nonce but token nonce does not match' do
|
344
|
+
payload = {
|
345
|
+
iss: "https://#{domain}/",
|
346
|
+
sub: 'sub',
|
347
|
+
aud: client_id,
|
348
|
+
exp: future_timecode,
|
349
|
+
iat: past_timecode,
|
350
|
+
nonce: 'mismatch'
|
351
|
+
}
|
352
|
+
|
353
|
+
token = make_hs256_token(payload)
|
354
|
+
expect do
|
355
|
+
jwt_validator.verify(token, { nonce: 'noncey' })
|
356
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
357
|
+
message: "Nonce (nonce) claim value mismatch in the ID token; expected (noncey), found (mismatch)"
|
358
|
+
}))
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'should fail when “aud” is an array of strings and azp claim is not present' do
|
362
|
+
aud = [
|
363
|
+
client_id,
|
364
|
+
"https://#{domain}/userinfo"
|
365
|
+
]
|
366
|
+
payload = {
|
367
|
+
iss: "https://#{domain}/",
|
368
|
+
sub: 'sub',
|
369
|
+
aud: aud,
|
370
|
+
exp: future_timecode,
|
371
|
+
iat: past_timecode
|
372
|
+
}
|
373
|
+
|
374
|
+
token = make_hs256_token(payload)
|
375
|
+
expect do
|
376
|
+
jwt_validator.verify(token)
|
377
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
378
|
+
message: "Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values"
|
379
|
+
}))
|
380
|
+
end
|
381
|
+
|
382
|
+
it 'should fail when "azp" claim doesnt match the expected aud' do
|
383
|
+
aud = [
|
384
|
+
client_id,
|
385
|
+
"https://#{domain}/userinfo"
|
386
|
+
]
|
387
|
+
payload = {
|
388
|
+
iss: "https://#{domain}/",
|
389
|
+
sub: 'sub',
|
390
|
+
aud: aud,
|
391
|
+
exp: future_timecode,
|
392
|
+
iat: past_timecode,
|
393
|
+
azp: 'not_expected'
|
394
|
+
}
|
395
|
+
|
396
|
+
token = make_hs256_token(payload)
|
397
|
+
expect do
|
398
|
+
jwt_validator.verify(token)
|
399
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
400
|
+
message: "Authorized Party (azp) claim mismatch in the ID token; expected (#{client_id}), found (not_expected)"
|
401
|
+
}))
|
402
|
+
end
|
403
|
+
|
404
|
+
it 'should fail when “max_age” sent on the authentication request and this claim is not present' do
|
405
|
+
payload = {
|
406
|
+
iss: "https://#{domain}/",
|
407
|
+
sub: 'sub',
|
408
|
+
aud: client_id,
|
409
|
+
exp: future_timecode,
|
410
|
+
iat: past_timecode
|
411
|
+
}
|
412
|
+
|
413
|
+
token = make_hs256_token(payload)
|
414
|
+
expect do
|
415
|
+
jwt_validator.verify(token, { max_age: 60 })
|
416
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
417
|
+
message: "Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified"
|
418
|
+
}))
|
419
|
+
end
|
420
|
+
|
421
|
+
it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future' do
|
422
|
+
payload = {
|
423
|
+
iss: "https://#{domain}/",
|
424
|
+
sub: 'sub',
|
425
|
+
aud: client_id,
|
426
|
+
exp: future_timecode,
|
427
|
+
iat: past_timecode,
|
428
|
+
auth_time: past_timecode
|
429
|
+
}
|
430
|
+
|
431
|
+
token = make_hs256_token(payload)
|
432
|
+
expect do
|
433
|
+
jwt_validator.verify(token, { max_age: 60 })
|
434
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
435
|
+
message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(past_timecode + 60 + 60)})"
|
436
|
+
}))
|
437
|
+
end
|
438
|
+
|
439
|
+
it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
|
440
|
+
now = Time.now.to_i
|
441
|
+
auth_time = now - 121
|
442
|
+
max_age = 60
|
443
|
+
payload = {
|
444
|
+
iss: "https://#{domain}/",
|
445
|
+
sub: 'sub',
|
446
|
+
aud: client_id,
|
447
|
+
exp: future_timecode,
|
448
|
+
iat: past_timecode,
|
449
|
+
auth_time: auth_time
|
450
|
+
}
|
451
|
+
|
452
|
+
token = make_hs256_token(payload)
|
453
|
+
expect do
|
454
|
+
jwt_validator.verify(token, { max_age: max_age })
|
455
|
+
# Time.at(auth_time + max_age + leeway
|
456
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
457
|
+
message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + 60)})"
|
458
|
+
}))
|
459
|
+
end
|
460
|
+
|
461
|
+
it 'should verify when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
|
462
|
+
now = Time.now.to_i
|
463
|
+
auth_time = now - 119
|
464
|
+
max_age = 60
|
465
|
+
payload = {
|
466
|
+
iss: "https://#{domain}/",
|
467
|
+
sub: 'sub',
|
468
|
+
aud: client_id,
|
469
|
+
exp: future_timecode,
|
470
|
+
iat: past_timecode,
|
471
|
+
auth_time: auth_time
|
472
|
+
}
|
473
|
+
|
474
|
+
token = make_hs256_token(payload)
|
475
|
+
id_token = jwt_validator.verify(token, { max_age: max_age })
|
476
|
+
expect(id_token['auth_time']).to eq(auth_time)
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'should fail when authorize params has organization but org_id is missing in the token' do
|
480
|
+
payload = {
|
481
|
+
iss: "https://#{domain}/",
|
482
|
+
sub: 'sub',
|
483
|
+
aud: client_id,
|
484
|
+
exp: future_timecode,
|
485
|
+
iat: past_timecode
|
486
|
+
}
|
487
|
+
|
488
|
+
token = make_hs256_token(payload)
|
489
|
+
expect do
|
490
|
+
jwt_validator.verify(token, { organization: 'Test Org' })
|
491
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
492
|
+
message: "Organization Id (org_id) claim must be a string present in the ID token"
|
493
|
+
}))
|
494
|
+
end
|
495
|
+
|
496
|
+
it 'should fail when authorize params has organization but token org_id does not match' do
|
497
|
+
payload = {
|
498
|
+
iss: "https://#{domain}/",
|
499
|
+
sub: 'sub',
|
500
|
+
aud: client_id,
|
501
|
+
exp: future_timecode,
|
502
|
+
iat: past_timecode,
|
503
|
+
org_id: 'Wrong Org'
|
504
|
+
}
|
505
|
+
|
506
|
+
token = make_hs256_token(payload)
|
507
|
+
expect do
|
508
|
+
jwt_validator.verify(token, { organization: 'Test Org' })
|
509
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
510
|
+
message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'Test Org', found 'Wrong Org'"
|
511
|
+
}))
|
512
|
+
end
|
513
|
+
|
514
|
+
it 'should fail for RS256 token when kid is incorrect' do
|
515
|
+
domain = 'example.org'
|
516
|
+
sub = 'abc123'
|
517
|
+
payload = {
|
518
|
+
sub: sub,
|
519
|
+
exp: future_timecode,
|
520
|
+
iss: "https://#{domain}/",
|
521
|
+
iat: past_timecode,
|
522
|
+
aud: client_id
|
523
|
+
}
|
524
|
+
invalid_kid = 'invalid-kid'
|
525
|
+
token = make_rs256_token(payload, invalid_kid)
|
526
|
+
expect do
|
527
|
+
verified_token = make_jwt_validator(opt_domain: domain).verify(token)
|
528
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
529
|
+
message: "Could not find a public key for Key ID (kid) 'invalid-kid'"
|
530
|
+
}))
|
531
|
+
end
|
532
|
+
|
533
|
+
it 'should fail when RS256 token has invalid signature' do
|
534
|
+
domain = 'example.org'
|
535
|
+
sub = 'abc123'
|
536
|
+
payload = {
|
537
|
+
sub: sub,
|
538
|
+
exp: future_timecode,
|
539
|
+
iss: "https://#{domain}/",
|
540
|
+
iat: past_timecode,
|
541
|
+
aud: client_id
|
542
|
+
}
|
543
|
+
token = make_rs256_token(payload) + 'bad'
|
544
|
+
expect do
|
545
|
+
verified_token = make_jwt_validator(opt_domain: domain).verify(token)
|
546
|
+
end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({
|
547
|
+
message: "Signature verification failed"
|
548
|
+
}))
|
549
|
+
end
|
550
|
+
|
551
|
+
it 'should fail when algorithm is not RS256 or HS256' do
|
552
|
+
payload = {
|
553
|
+
iss: "https://#{domain}/",
|
554
|
+
sub: 'abc123',
|
555
|
+
aud: client_id,
|
556
|
+
exp: future_timecode,
|
557
|
+
iat: past_timecode
|
558
|
+
}
|
559
|
+
token = JWT.encode payload, 'secret', 'HS384'
|
560
|
+
expect do
|
561
|
+
jwt_validator.verify(token)
|
562
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
563
|
+
message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256"
|
564
|
+
}))
|
565
|
+
end
|
566
|
+
|
567
|
+
it 'should fail when HS256 token has invalid signature' do
|
568
|
+
payload = {
|
569
|
+
iss: "https://#{domain}/",
|
570
|
+
sub: 'abc123',
|
571
|
+
aud: client_id,
|
572
|
+
exp: future_timecode,
|
573
|
+
iat: past_timecode
|
574
|
+
}
|
575
|
+
token = make_hs256_token(payload, 'bad_secret')
|
576
|
+
expect do
|
577
|
+
# validator is configured to use "CLIENT_SECRET" by default
|
578
|
+
jwt_validator.verify(token)
|
579
|
+
end.to raise_error(an_instance_of(JWT::VerificationError))
|
580
|
+
end
|
581
|
+
|
582
|
+
it 'should verify a valid HS256 token with multiple audiences' do
|
583
|
+
audience = [
|
584
|
+
client_id,
|
585
|
+
"https://#{domain}/userinfo"
|
586
|
+
]
|
587
|
+
payload = {
|
588
|
+
iss: "https://#{domain}/",
|
589
|
+
sub: 'sub',
|
590
|
+
aud: audience,
|
591
|
+
exp: future_timecode,
|
592
|
+
iat: past_timecode,
|
593
|
+
azp: client_id
|
594
|
+
}
|
595
|
+
token = make_hs256_token(payload)
|
596
|
+
id_token = jwt_validator.verify(token)
|
597
|
+
expect(id_token['aud']).to eq(audience)
|
598
|
+
end
|
599
|
+
|
600
|
+
it 'should verify a standard HS256 token' do
|
601
|
+
sub = 'abc123'
|
602
|
+
payload = {
|
603
|
+
iss: "https://#{domain}/",
|
604
|
+
sub: sub,
|
605
|
+
aud: client_id,
|
606
|
+
exp: future_timecode,
|
607
|
+
iat: past_timecode
|
608
|
+
}
|
609
|
+
token = make_hs256_token(payload)
|
610
|
+
verified_token = jwt_validator.verify(token)
|
611
|
+
expect(verified_token['sub']).to eq(sub)
|
612
|
+
end
|
613
|
+
|
614
|
+
it 'should verify a standard RS256 token' do
|
615
|
+
domain = 'example.org'
|
616
|
+
sub = 'abc123'
|
617
|
+
payload = {
|
618
|
+
sub: sub,
|
619
|
+
exp: future_timecode,
|
620
|
+
iss: "https://#{domain}/",
|
621
|
+
iat: past_timecode,
|
622
|
+
aud: client_id
|
623
|
+
}
|
624
|
+
token = make_rs256_token(payload)
|
625
|
+
verified_token = make_jwt_validator(opt_domain: domain).verify(token)
|
626
|
+
expect(verified_token['sub']).to eq(sub)
|
627
|
+
end
|
628
|
+
|
629
|
+
it 'should verify a HS256 JWT signature when calling verify signature directly' do
|
630
|
+
sub = 'abc123'
|
631
|
+
payload = {
|
632
|
+
iss: "https://#{domain}/",
|
633
|
+
sub: sub,
|
634
|
+
aud: client_id,
|
635
|
+
exp: future_timecode,
|
636
|
+
iat: past_timecode
|
637
|
+
}
|
638
|
+
token = make_hs256_token(payload)
|
639
|
+
verified_token_signature = jwt_validator.verify_signature(token)
|
640
|
+
expect(verified_token_signature[0]).to eq('CLIENT_SECRET')
|
641
|
+
expect(verified_token_signature[1]).to eq('HS256')
|
642
|
+
end
|
643
|
+
|
644
|
+
it 'should verify a RS256 JWT signature verify signature directly' do
|
645
|
+
domain = 'example.org'
|
646
|
+
sub = 'abc123'
|
647
|
+
payload = {
|
648
|
+
sub: sub,
|
649
|
+
exp: future_timecode,
|
650
|
+
iss: "https://#{domain}/",
|
651
|
+
iat: past_timecode,
|
652
|
+
aud: client_id
|
653
|
+
}
|
654
|
+
token = make_rs256_token(payload)
|
655
|
+
verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token)
|
656
|
+
expect(verified_token_signature.length).to be(2)
|
657
|
+
expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA)
|
658
|
+
expect(verified_token_signature[1]).to eq('RS256')
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
private
|
663
|
+
|
664
|
+
def make_jwt_validator(opt_domain: domain, opt_issuer: nil)
|
665
|
+
opts = OpenStruct.new(
|
666
|
+
domain: opt_domain,
|
667
|
+
client_id: client_id,
|
668
|
+
client_secret: client_secret
|
669
|
+
)
|
670
|
+
opts[:issuer] = opt_issuer unless opt_issuer.nil?
|
671
|
+
|
672
|
+
OmniAuth::Auth0::JWTValidator.new(opts)
|
673
|
+
end
|
674
|
+
|
675
|
+
def make_hs256_token(payload = nil, secret = nil)
|
676
|
+
payload = { sub: 'abc123' } if payload.nil?
|
677
|
+
secret = client_secret if secret.nil?
|
678
|
+
JWT.encode payload, secret, 'HS256'
|
679
|
+
end
|
680
|
+
|
681
|
+
def make_rs256_token(payload = nil, kid = nil)
|
682
|
+
payload = { sub: 'abc123' } if payload.nil?
|
683
|
+
kid = valid_jwks_kid if kid.nil?
|
684
|
+
JWT.encode payload, rsa_private_key, 'RS256', kid: kid
|
685
|
+
end
|
686
|
+
|
687
|
+
def make_cert(private_key)
|
688
|
+
cert = OpenSSL::X509::Certificate.new
|
689
|
+
cert.issuer = OpenSSL::X509::Name.parse('/C=BE/O=Auth0/OU=Auth0/CN=Auth0')
|
690
|
+
cert.subject = cert.issuer
|
691
|
+
cert.not_before = Time.now
|
692
|
+
cert.not_after = Time.now + 365 * 24 * 60 * 60
|
693
|
+
cert.public_key = private_key.public_key
|
694
|
+
cert.serial = 0x0
|
695
|
+
cert.version = 2
|
696
|
+
|
697
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
698
|
+
ef.subject_certificate = cert
|
699
|
+
ef.issuer_certificate = cert
|
700
|
+
cert.extensions = [
|
701
|
+
ef.create_extension('basicConstraints', 'CA:TRUE', true),
|
702
|
+
ef.create_extension('subjectKeyIdentifier', 'hash')
|
703
|
+
]
|
704
|
+
cert.add_extension ef.create_extension(
|
705
|
+
'authorityKeyIdentifier',
|
706
|
+
'keyid:always,issuer:always'
|
707
|
+
)
|
708
|
+
|
709
|
+
cert.sign private_key, OpenSSL::Digest::SHA1.new
|
710
|
+
end
|
711
|
+
|
712
|
+
def stub_complete_jwks
|
713
|
+
stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json')
|
714
|
+
.to_return(
|
715
|
+
headers: { 'Content-Type' => 'application/json' },
|
716
|
+
body: jwks.to_json,
|
717
|
+
status: 200
|
718
|
+
)
|
719
|
+
end
|
720
|
+
|
721
|
+
def stub_expected_jwks
|
722
|
+
stub_request(:get, 'https://example.org/.well-known/jwks.json')
|
723
|
+
.to_return(
|
724
|
+
headers: { 'Content-Type' => 'application/json' },
|
725
|
+
body: valid_jwks,
|
726
|
+
status: 200
|
727
|
+
)
|
728
|
+
end
|
729
|
+
end
|