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.
@@ -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