omniauth-auth0 2.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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