auth0 4.11.0 → 4.12.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +37 -30
- data/README.md +45 -0
- data/auth0.gemspec +2 -0
- data/lib/auth0.rb +1 -0
- data/lib/auth0/algorithm.rb +5 -0
- data/lib/auth0/api/authentication_endpoints.rb +34 -0
- data/lib/auth0/exception.rb +2 -0
- data/lib/auth0/mixins/validation.rb +315 -0
- data/lib/auth0/version.rb +1 -1
- data/spec/lib/auth0/api/authentication_endpoints_spec.rb +35 -10
- data/spec/lib/auth0/mixins/validation_spec.rb +474 -0
- data/spec/spec_helper.rb +1 -0
- metadata +33 -2
data/lib/auth0/version.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# rubocop:disable Metrics/BlockLength
|
|
2
4
|
require 'spec_helper'
|
|
3
5
|
describe Auth0::Api::AuthenticationEndpoints do
|
|
@@ -20,7 +22,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
20
22
|
grant_type: 'client_credentials',
|
|
21
23
|
client_id: @instance.client_id,
|
|
22
24
|
client_secret: @instance.client_secret,
|
|
23
|
-
audience: @instance.audience
|
|
25
|
+
audience: @instance.audience
|
|
24
26
|
).and_return('access_token' => 'AccessToken')
|
|
25
27
|
|
|
26
28
|
expect(@instance.api_token.token).to eql 'AccessToken'
|
|
@@ -32,7 +34,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
32
34
|
grant_type: 'client_credentials',
|
|
33
35
|
client_id: @instance.client_id,
|
|
34
36
|
client_secret: @instance.client_secret,
|
|
35
|
-
audience: '__test_audience__'
|
|
37
|
+
audience: '__test_audience__'
|
|
36
38
|
).and_return('access_token' => 'AccessToken')
|
|
37
39
|
|
|
38
40
|
expect(
|
|
@@ -47,7 +49,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
47
49
|
allow(@instance).to receive(:post).with(
|
|
48
50
|
'/oauth/token', client_id: @instance.client_id, client_secret: @instance.client_secret, grant_type: 'client_credentials'
|
|
49
51
|
)
|
|
50
|
-
|
|
52
|
+
.and_return('access_token' => 'AccessToken')
|
|
51
53
|
|
|
52
54
|
expect(@instance).to receive(:post).with(
|
|
53
55
|
'/oauth/token', client_id: @instance.client_id, client_secret: @instance.client_secret, grant_type: 'client_credentials'
|
|
@@ -90,7 +92,6 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
90
92
|
it { expect { @instance.obtain_user_tokens('code', '') }.to raise_error 'Must supply a valid redirect_uri' }
|
|
91
93
|
end
|
|
92
94
|
|
|
93
|
-
|
|
94
95
|
context '.exchange_auth_code_for_tokens' do
|
|
95
96
|
it { is_expected.to respond_to(:exchange_auth_code_for_tokens) }
|
|
96
97
|
|
|
@@ -183,7 +184,6 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
183
184
|
).to eq 'AccessToken'
|
|
184
185
|
end
|
|
185
186
|
|
|
186
|
-
|
|
187
187
|
it 'is expected to make post request to /oauth/token with custom params' do
|
|
188
188
|
allow(@instance).to receive(:post).with(
|
|
189
189
|
'/oauth/token',
|
|
@@ -241,7 +241,6 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
it 'should make post to /oauth/token with custom params' do
|
|
244
|
-
|
|
245
244
|
allow(@instance).to receive(:post).with(
|
|
246
245
|
'/oauth/token',
|
|
247
246
|
username: 'test@test.com',
|
|
@@ -356,7 +355,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
356
355
|
'/passwordless/start',
|
|
357
356
|
client_id: @instance.client_id,
|
|
358
357
|
client_secret: @instance.client_secret,
|
|
359
|
-
connection:
|
|
358
|
+
connection: 'email',
|
|
360
359
|
email: 'test@test.com',
|
|
361
360
|
send: 'code',
|
|
362
361
|
authParams: {
|
|
@@ -552,7 +551,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
552
551
|
context '.userinfo' do
|
|
553
552
|
it { is_expected.to respond_to(:user_info) }
|
|
554
553
|
it 'is expected to make a GET request to /userinfo' do
|
|
555
|
-
is_expected.to receive(:get).with('/userinfo', {}, {'Authorization' => 'Bearer access-token'})
|
|
554
|
+
is_expected.to receive(:get).with('/userinfo', {}, { 'Authorization' => 'Bearer access-token' })
|
|
556
555
|
subject.userinfo 'access-token'
|
|
557
556
|
end
|
|
558
557
|
end
|
|
@@ -605,7 +604,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
605
604
|
|
|
606
605
|
it 'is expected to return a logout url with a client ID' do
|
|
607
606
|
expect(@instance.logout_url(return_to, include_client: true).to_s).to eq(
|
|
608
|
-
"https://#{@instance.domain}/v2/logout"
|
|
607
|
+
"https://#{@instance.domain}/v2/logout" \
|
|
609
608
|
"?returnTo=http%3A%2F%2Freturnto.com&client_id=#{@instance.client_id}"
|
|
610
609
|
)
|
|
611
610
|
end
|
|
@@ -655,7 +654,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
655
654
|
end
|
|
656
655
|
|
|
657
656
|
it 'is expected to get the wsfed url with wctx' do
|
|
658
|
-
expect(@instance.wsfed_url(UP_AUTH, {wctx: 'wctx_test'}).to_s).to eq(
|
|
657
|
+
expect(@instance.wsfed_url(UP_AUTH, { wctx: 'wctx_test' }).to_s).to eq(
|
|
659
658
|
"https://#{@instance.domain}/wsfed/#{@instance.client_id}" \
|
|
660
659
|
"?whr=#{UP_AUTH}&wctx=wctx_test"
|
|
661
660
|
)
|
|
@@ -674,5 +673,31 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
|
674
673
|
)
|
|
675
674
|
end
|
|
676
675
|
end
|
|
676
|
+
|
|
677
|
+
# Auth0::API::AuthenticationEndpoints.validate_id_token
|
|
678
|
+
context '.validate_id_token' do
|
|
679
|
+
it { expect(@instance).to respond_to(:validate_id_token) }
|
|
680
|
+
|
|
681
|
+
it 'is expected not to raise an error with default values' do
|
|
682
|
+
stub_request(:get, 'https://test.auth0.com/.well-known/jwks.json').to_return(body: JWKS_RESPONSE_1.to_json)
|
|
683
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL3Rlc3QuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDEyMzQ1Njc4OSIsImF1ZCI6WyJfX3Rlc3RfYXVkaWVuY2VfXyIsIl9fdGVzdF9jbGllbnRfaWRfXyJdLCJleHAiOjI1MzgzMDExNDYsImlhdCI6MTU4NzU5MjU2MSwiYXpwIjoiX190ZXN0X2NsaWVudF9pZF9fIn0.X35Hfa1C9RtuJIj7Eky2iO4elY9XqCDRy8ieFAft63vGds9vhP38x8QHbJifmLs6vDEOySKfJMWhklp3oaXm6Tk6gyUQEaliW_pXUgZt8C3Xo125R8BMCDQeVJg8Abevbg6FpHpYztWpQuI609tmpoTczx7pXMmAneg6e4LNYvvtzaFD_0M0cxtjkm4OcevCJszNBru3tdXwRynkGbMYeXgoa_FumAshRvIvh-4dtkyNWsepo5IVTvixxF3FVoFaXOOycmFXh9gxOppG4lvE78AFB9AQ9LNS-DNhcXszbPs9KHMrg2bqhSL8Razqd3m2a1MXkdLMBD5DY499MVnb5w'
|
|
684
|
+
|
|
685
|
+
expect { @instance.validate_id_token(token) }.to_not raise_exception
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
it 'is expected not to raise an error with custom values' do
|
|
689
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsiYXVkaWVuY2UiLCJhbm90aGVyX2F1ZGllbmNlIl0sImV4cCI6MjUzODMwMTE0NiwiaWF0IjoxNTg3NTkyNTYxLCJub25jZSI6Im5vbmNlIiwiYXpwIjoiYXVkaWVuY2UiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.u39qTvuUmbzj5jsXjATXxjxJt0u064G1IAumoi18gm0'
|
|
690
|
+
|
|
691
|
+
expect do
|
|
692
|
+
@instance.validate_id_token(token,
|
|
693
|
+
algorithm: Auth0::Algorithm::HS256.secret('secret'),
|
|
694
|
+
leeway: 100,
|
|
695
|
+
nonce: 'nonce',
|
|
696
|
+
max_age: 2538301146,
|
|
697
|
+
issuer: 'issuer',
|
|
698
|
+
audience: 'audience')
|
|
699
|
+
end.to_not raise_exception
|
|
700
|
+
end
|
|
701
|
+
end
|
|
677
702
|
end
|
|
678
703
|
# rubocop:enable Metrics/BlockLength
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# rubocop:disable Metrics/BlockLength
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
RSA_PUB_KEY_JWK_1 = { 'kty': "RSA", 'use': 'sig', 'n': "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", 'e': 'AQAB', 'kid': 'test-key-1' }.freeze
|
|
5
|
+
RSA_PUB_KEY_JWK_2 = { 'kty': "RSA", 'use': 'sig', 'n': "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", 'e': 'AQAB', 'kid': 'test-key-2' }.freeze
|
|
6
|
+
JWKS_RESPONSE_1 = { 'keys': [RSA_PUB_KEY_JWK_1] }.freeze
|
|
7
|
+
JWKS_RESPONSE_2 = { 'keys': [RSA_PUB_KEY_JWK_2] }.freeze
|
|
8
|
+
JWKS_URL = 'https://tokens-test.auth0.com/.well-known/jwks.json'.freeze
|
|
9
|
+
HMAC_SHARED_SECRET = 'secret'.freeze
|
|
10
|
+
|
|
11
|
+
LEEWAY = 60
|
|
12
|
+
CLOCK = 1587592561 # Apr 22 2020 21:56:01 UTC
|
|
13
|
+
CONTEXT = { algorithm: Auth0::Algorithm::HS256.secret(HMAC_SHARED_SECRET), leeway: LEEWAY, audience: 'tokens-test-123', issuer: 'https://tokens-test.auth0.com/', clock: CLOCK }.freeze
|
|
14
|
+
|
|
15
|
+
describe Auth0::Mixins::Validation::IdTokenValidator do
|
|
16
|
+
subject { @instance }
|
|
17
|
+
|
|
18
|
+
context 'instance' do
|
|
19
|
+
it 'is expected respond to :validate' do
|
|
20
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new({})
|
|
21
|
+
|
|
22
|
+
expect(instance).to respond_to(:validate)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'ID token decoding' do
|
|
27
|
+
expected_error = 'ID token could not be decoded'
|
|
28
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new({})
|
|
29
|
+
|
|
30
|
+
it 'is expected to raise an error with a nil id_token' do
|
|
31
|
+
expect { instance.validate(nil) }.to raise_exception(expected_error)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'is expected to raise an error with an empty id_token' do
|
|
35
|
+
expect { instance.validate('') }.to raise_exception(expected_error)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'is expected to raise an error with an invalid format' do
|
|
39
|
+
expect { instance.validate('a.b') }.to raise_exception(expected_error)
|
|
40
|
+
expect { instance.validate('a.b.') }.to raise_exception(expected_error)
|
|
41
|
+
expect { instance.validate('a.b.c.d') }.to raise_exception(expected_error)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'is expected to raise an error with an invalid encoding' do
|
|
45
|
+
expect { instance.validate('a.b.c') }.to raise_exception(expected_error)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'algorithm verification' do
|
|
50
|
+
token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Hn38QVtN_mWN0c-jOa-Fqq69kXpbBp0THsvE-CQ47Ps'
|
|
51
|
+
|
|
52
|
+
it 'is expected to raise an error with an unsupported algorithm' do
|
|
53
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new({ algorithm: 'ES256' })
|
|
54
|
+
|
|
55
|
+
expect { instance.validate(token) }.to raise_exception('Signature algorithm of "ES256" is not supported')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'is expected to raise an error when the algorithm does not match the alg header value' do
|
|
59
|
+
algorithm = Auth0::Algorithm::HS256.secret(HMAC_SHARED_SECRET)
|
|
60
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new({ algorithm: algorithm })
|
|
61
|
+
|
|
62
|
+
expect { instance.validate(token) }.to raise_exception('Signature algorithm of "ES256" is not supported. Expected the ID token to be signed with "HS256"')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'HS256 signature verification' do
|
|
67
|
+
before :each do
|
|
68
|
+
algorithm = Auth0::Algorithm::HS256.secret(HMAC_SHARED_SECRET)
|
|
69
|
+
@instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ algorithm: algorithm }))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'is expected not to raise an error with a valid signature' do
|
|
73
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Hn38QVtN_mWN0c-jOa-Fqq69kXpbBp0THsvE-CQ47Ps'
|
|
74
|
+
|
|
75
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'is expected to raise an error with an invalid signature' do
|
|
79
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.invalidsignature'
|
|
80
|
+
|
|
81
|
+
expect { @instance.validate(token) }.to raise_exception('Invalid ID token signature')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context 'RS256 signature verification' do
|
|
86
|
+
before :each do
|
|
87
|
+
stub_jwks
|
|
88
|
+
algorithm = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
89
|
+
@instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ algorithm: algorithm }))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
after :each do
|
|
93
|
+
Auth0::Algorithm::RS256.remove_jwks
|
|
94
|
+
WebMock.reset!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'is expected not to raise an error with a valid signature' do
|
|
98
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.jE00ARUiAwrKEoAMwbioKYj4bUZjmg31V7McDtIPsJJ16rYcvI-e5mtSSMgCmAom6t-WA7dsSWCJUlBCW2nAMvyCZ-hj8HG9Z0RmQEiwig9Fk22avoX94zdx65TwAeDfn2uMRaX_ps3TJcn4nymUtMp8Lps_vMw15eJerKThlSO4KuLTrvDDdRaCRamAd7jxuzhiwOt0mB0TVD55b5itA02pGuyapbjQXvvLYEx8OgpyIaAkB9Ry25abgjev0bSw2kjFZckG3lv9QgvZM85m9l3Rbrc6msNPGfMDFWGyT3Tu2ObqnSEA-57hZeuCbFrOya3vUwgSlc66rfvZj2xpzg'
|
|
99
|
+
|
|
100
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'is expected to raise an error with an invalid signature' do
|
|
104
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.invalidsignature'
|
|
105
|
+
|
|
106
|
+
expect { @instance.validate(token) }.to raise_exception('Invalid ID token signature')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'is expected to raise an error when the public key cannot be found' do
|
|
110
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTIifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.r2ksgiiM8zGJ6byea_Fq_zWWEmUdOnwQLVdb5JzgdBv1GUQFp-4LNaRhcga4FIrbKgxaPeewGLTq2VqfjmNJUNfARcE3QEacQ_JEHkC6zKZIiqcu4msHl8X9HXyHxOPHMTTjYMjauPzET7UH_oLxF68DDDQqvKX9VqLsncpyC-KdTCFTLGlFSq6pxmYt6gwrF2Uo15Gzc6qe2I9-MTXCYd44VW1zQi6GhNJNKbXH6U3bf7nof0ot1PSjBXXuLgf6d3qumTStECCjIUmdBb6FiEX4SSRI4MgHWj4q0LyN28baRpYwYPhVnjzUxOP7OLjKiHs45ORBhuAWhrJnuR_uBQ'
|
|
111
|
+
|
|
112
|
+
expect { @instance.validate(token) }.to raise_exception('Could not find a public key for Key ID (kid) "test-key-2"')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'is expected to fetch the JWK set from the url if the public key cannot be found and the cache is not empty' do
|
|
116
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTIifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.r2ksgiiM8zGJ6byea_Fq_zWWEmUdOnwQLVdb5JzgdBv1GUQFp-4LNaRhcga4FIrbKgxaPeewGLTq2VqfjmNJUNfARcE3QEacQ_JEHkC6zKZIiqcu4msHl8X9HXyHxOPHMTTjYMjauPzET7UH_oLxF68DDDQqvKX9VqLsncpyC-KdTCFTLGlFSq6pxmYt6gwrF2Uo15Gzc6qe2I9-MTXCYd44VW1zQi6GhNJNKbXH6U3bf7nof0ot1PSjBXXuLgf6d3qumTStECCjIUmdBb6FiEX4SSRI4MgHWj4q0LyN28baRpYwYPhVnjzUxOP7OLjKiHs45ORBhuAWhrJnuR_uBQ'
|
|
117
|
+
Auth0::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
|
118
|
+
stub_jwks(JWKS_RESPONSE_2)
|
|
119
|
+
@instance.validate(token)
|
|
120
|
+
|
|
121
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'context validation' do
|
|
126
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Hn38QVtN_mWN0c-jOa-Fqq69kXpbBp0THsvE-CQ47Ps'
|
|
127
|
+
|
|
128
|
+
it 'is expected to raise an error with a non-integer leeway' do
|
|
129
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ leeway: '1' }))
|
|
130
|
+
|
|
131
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid leeway')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'is expected to raise an error with a negative leeway' do
|
|
135
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ leeway: -1 }))
|
|
136
|
+
|
|
137
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid leeway')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'is expected to raise an error with an empty nonce' do
|
|
141
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: '' }))
|
|
142
|
+
|
|
143
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid nonce')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'is expected to raise an error with an empty issuer' do
|
|
147
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ issuer: '' }))
|
|
148
|
+
|
|
149
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid issuer')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'is expected to raise an error with an empty audience' do
|
|
153
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ audience: '' }))
|
|
154
|
+
|
|
155
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid audience')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'is expected to raise an error with a non-integer max_age' do
|
|
159
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: '1' }))
|
|
160
|
+
|
|
161
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid max_age')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'is expected to raise an error with a negative max_age' do
|
|
165
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: -1 }))
|
|
166
|
+
|
|
167
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid max_age')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context 'claims validation' do
|
|
172
|
+
before :all do
|
|
173
|
+
@instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'is expected to raise an error with a missing iss' do
|
|
177
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.QL2B2tqJhlW9rc8HQ3PQKkjDufBeSvtRBtJmRPdQ5K8'
|
|
178
|
+
|
|
179
|
+
expect { @instance.validate(token) }.to raise_exception('Issuer (iss) claim must be a string present in the ID token')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'is expected to raise an error with a invalid iss' do
|
|
183
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb21ldGhpbmctZWxzZSIsInN1YiI6ImF1dGgwfDEyMzQ1Njc4OSIsImF1ZCI6WyJ0b2tlbnMtdGVzdC0xMjMiLCJleHRlcm5hbC10ZXN0LTk5OSJdLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.AhMMouDlGMdxTYrY9Cn-p8svJ8ssKmsHeT6JNRVTh10'
|
|
184
|
+
|
|
185
|
+
expect { @instance.validate(token) }.to raise_exception("Issuer (iss) claim mismatch in the ID token; expected \"#{CONTEXT[:issuer]}\", found \"something-else\"")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'is expected to raise an error with a missing sub' do
|
|
189
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0._4sUXtAZYpGrO3QaYArXnk2JivCqixa7hgHhH3w4SlY'
|
|
190
|
+
|
|
191
|
+
expect { @instance.validate(token) }.to raise_exception('Subject (sub) claim must be a string present in the ID token')
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'is expected to raise an error with a missing aud' do
|
|
195
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.TlwnBmGUKe0SElSYKxPqsG1mujkK2t1CwDJGGiWRdXA'
|
|
196
|
+
|
|
197
|
+
expect { @instance.validate(token) }.to raise_exception('Audience (aud) claim must be a string or array of strings present in the ID token')
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'is expected to raise an error with an invalid string aud' do
|
|
201
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOiJleHRlcm5hbC10ZXN0LTk5OSIsImV4cCI6MTU4Nzc2NTM2MSwiaWF0IjoxNTg3NTkyNTYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.-Tf5CIi2bZ51UdgqxFWQNXpJJmK5GgsetcVoVrQwHIA'
|
|
202
|
+
|
|
203
|
+
expect { @instance.validate(token) }.to raise_exception("Audience (aud) claim mismatch in the ID token; expected \"#{CONTEXT[:audience]}\", found \"external-test-999\"")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'is expected to raise an error with an invalid array aud' do
|
|
207
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsiZXh0ZXJuYWwtdGVzdC05OTgiLCJleHRlcm5hbC10ZXN0LTk5OSJdLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.Q1GRVL5g3RLQqG5sEV_cc8WW_oiZzFIAfzRfBdxMW2s'
|
|
208
|
+
|
|
209
|
+
expect { @instance.validate(token) }.to raise_exception("Audience (aud) claim mismatch in the ID token; expected \"#{CONTEXT[:audience]}\" but was not one of \"external-test-998, external-test-999\"")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'is expected to raise an error with a missing exp' do
|
|
213
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiaWF0IjoxNTg3NTkyNTYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.aoLiQX3sHsf1bEbc0axbjJ9qV6hhomtEzJq-FT8OGF0'
|
|
214
|
+
|
|
215
|
+
expect { @instance.validate(token) }.to raise_exception('Expiration Time (exp) claim must be a number present in the ID token')
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'is expected to raise an error with a invalid exp' do
|
|
219
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NTkyNTYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.A8Pc0vlCG5Ufez7VIoRqXTYpJehalTEgGX9cR2xJLkU'
|
|
220
|
+
clock = CLOCK + LEEWAY + 1
|
|
221
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ clock: clock }))
|
|
222
|
+
|
|
223
|
+
expect { instance.validate(token) }.to raise_exception("Expiration Time (exp) claim mismatch in the ID token; current time \"#{clock}\" is after expiration time \"1587592621\"")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'is expected to raise an error with a missing iat' do
|
|
227
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.Jea6UVJsAK7Hnb494f_WIQCIbaLTnnCvMenSY1Y2toc'
|
|
228
|
+
|
|
229
|
+
expect { @instance.validate(token) }.to raise_exception('Issued At (iat) claim must be a number present in the ID token')
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'is expected to raise an error with a invalid iat' do
|
|
233
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc3NjUzNjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.1AeRLTaExbKnmsfNduUl3HArsau4RcNrnmYOJnkPWi0'
|
|
234
|
+
clock = CLOCK - LEEWAY - 1
|
|
235
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ clock: clock }))
|
|
236
|
+
|
|
237
|
+
expect { instance.validate(token) }.to raise_exception("Issued At (iat) claim mismatch in the ID token; current time \"#{clock}\" is before issued at time \"1587765301\"")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'is expected not to raise an error with a missing but not required nonce' do
|
|
241
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.-o5grnyODbBdRgzcrn7Sf9Hb6eOC0x_U2i3YjVgHN0U'
|
|
242
|
+
|
|
243
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'is expected to raise an error with a missing but required nonce' do
|
|
247
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.-o5grnyODbBdRgzcrn7Sf9Hb6eOC0x_U2i3YjVgHN0U'
|
|
248
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: 'a1b2c3d4e5' }))
|
|
249
|
+
|
|
250
|
+
expect { instance.validate(token) }.to raise_exception('Nonce (nonce) claim must be a string present in the ID token')
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'is expected to raise an error with an invalid nonce' do
|
|
254
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiMDAwOTk5IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.XqQPdFN4m5kmTUQQi_mAJu0LQOeUTS9lF2J_xWc_j-0'
|
|
255
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: 'a1b2c3d4e5' }))
|
|
256
|
+
|
|
257
|
+
expect { instance.validate(token) }.to raise_exception('Nonce (nonce) claim mismatch in the ID token; expected "a1b2c3d4e5", found "000999"')
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'is expected to raise an error with a missing azp' do
|
|
261
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.LrgYkIbWrxMq6jvvkL1lxWL237ii1IBhtN2o_tDxFns'
|
|
262
|
+
|
|
263
|
+
expect { @instance.validate(token) }.to raise_exception('Authorized Party (azp) claim must be a string present in the ID token')
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it 'is expected to raise an error with an invalid azp' do
|
|
267
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6ImV4dGVybmFsLXRlc3QtOTk5IiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.3DX-LY3B4UngDML-9nv11Sv89ECJpRpOLeWnkF1vAFY'
|
|
268
|
+
|
|
269
|
+
expect { @instance.validate(token) }.to raise_exception("Authorized Party (azp) claim mismatch in the ID token; expected \"tokens-test-123\", found \"external-test-999\"")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'is expected to raise an error with a missing auth_time' do
|
|
273
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyJ9.JqUotnjHbGW0FcHz1s1YsRkce9Sbpsv2AEBDMpcUhp8'
|
|
274
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: 120 }))
|
|
275
|
+
|
|
276
|
+
expect { instance.validate(token) }.to raise_exception('Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified')
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'is expected to raise an error with a invalid auth_time' do
|
|
280
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzU5MjU2MX0.B7eWHJPHjhOh0ALjIQi0ro6zVsqGIeHd0gpRZsv6Hg8'
|
|
281
|
+
max_age = 120
|
|
282
|
+
auth_time = CLOCK + LEEWAY + max_age
|
|
283
|
+
clock = auth_time + 1
|
|
284
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: max_age, clock: clock }))
|
|
285
|
+
|
|
286
|
+
expect { instance.validate(token) }.to raise_exception("Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time \"#{clock}\" is after last auth at \"#{auth_time}\"")
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
describe Auth0::Algorithm::HS256 do
|
|
292
|
+
context 'class' do
|
|
293
|
+
it 'is expected to respond to :secret' do
|
|
294
|
+
expect(Auth0::Algorithm::HS256).to respond_to(:secret)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it 'is expected not to respond to :new' do
|
|
298
|
+
expect(Auth0::Algorithm::HS256).not_to respond_to(:new)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
context 'instance' do
|
|
303
|
+
it 'is expected to respond to :secret' do
|
|
304
|
+
instance = Auth0::Algorithm::HS256.secret('secret')
|
|
305
|
+
|
|
306
|
+
expect(instance).to respond_to(:secret)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it 'is expected to return the secret' do
|
|
310
|
+
instance = Auth0::Algorithm::HS256.secret('secret')
|
|
311
|
+
|
|
312
|
+
expect(instance.secret).to eq('secret')
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'is expected to return the algorithm name' do
|
|
316
|
+
instance = Auth0::Algorithm::HS256.secret('secret')
|
|
317
|
+
|
|
318
|
+
expect(instance.name).to eq('HS256')
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
context 'parameters' do
|
|
323
|
+
expected_error = 'Must supply a valid secret'
|
|
324
|
+
|
|
325
|
+
it 'is expected to raise an error with a nil secret' do
|
|
326
|
+
expect { Auth0::Algorithm::HS256.secret(nil) }.to raise_exception(expected_error)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it 'is expected to raise an error with an empty secret' do
|
|
330
|
+
expect { Auth0::Algorithm::HS256.secret('') }.to raise_exception(expected_error)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
describe Auth0::Algorithm::RS256 do
|
|
336
|
+
before :each do
|
|
337
|
+
stub_jwks
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
after :each do
|
|
341
|
+
Auth0::Algorithm::RS256.remove_jwks
|
|
342
|
+
WebMock.reset!
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
context 'class' do
|
|
346
|
+
it 'is expected to respond to :jwks_url' do
|
|
347
|
+
expect(Auth0::Algorithm::RS256).to respond_to(:jwks_url)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it 'is expected not to respond to :new' do
|
|
351
|
+
expect(Auth0::Algorithm::RS256).not_to respond_to(:new)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
context 'instance' do
|
|
356
|
+
it 'is expected to respond to :jwks' do
|
|
357
|
+
instance = Auth0::Algorithm::RS256.jwks_url('jwks url')
|
|
358
|
+
|
|
359
|
+
expect(instance).to respond_to(:jwks)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it 'is expected to respond to :fetched_jwks?' do
|
|
363
|
+
instance = Auth0::Algorithm::RS256.jwks_url('jwks url')
|
|
364
|
+
|
|
365
|
+
expect(instance).to respond_to(:fetched_jwks?)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it 'is expected to return a jwks' do
|
|
369
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
370
|
+
|
|
371
|
+
expect(instance.jwks).to have_key('keys') and contain_exactly(a_hash_including(kid: 'test-key-1'))
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it 'is expected to return if the jwks was fetched from the url' do
|
|
375
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
376
|
+
instance.jwks
|
|
377
|
+
|
|
378
|
+
expect(instance.fetched_jwks?).to eq(true)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it 'is expected to return if the jwks was fetched from the cache' do
|
|
382
|
+
Auth0::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
|
383
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
384
|
+
instance.jwks
|
|
385
|
+
|
|
386
|
+
expect(instance.fetched_jwks?).to eq(false)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it 'is expected to return the algorithm name' do
|
|
390
|
+
instance = Auth0::Algorithm::RS256.jwks_url('jwks url')
|
|
391
|
+
|
|
392
|
+
expect(instance.name).to eq('RS256')
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
context 'parameters' do
|
|
397
|
+
it 'is expected to raise an error with a nil jwks_url' do
|
|
398
|
+
expect { Auth0::Algorithm::RS256.jwks_url(nil) }.to raise_exception('Must supply a valid jwks_url')
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it 'is expected to raise an error with an empty jwks_url' do
|
|
402
|
+
expect { Auth0::Algorithm::RS256.jwks_url('') }.to raise_exception('Must supply a valid jwks_url')
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it 'is expected to raise an error with a non-integer lifetime' do
|
|
406
|
+
expect { Auth0::Algorithm::RS256.jwks_url('jwks url', lifetime: '1') }.to raise_exception('Must supply a valid lifetime')
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
it 'is expected to raise an error with a negative lifetime' do
|
|
410
|
+
expect { Auth0::Algorithm::RS256.jwks_url('jwks url', lifetime: -1) }.to raise_exception('Must supply a valid lifetime')
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
context 'cache' do
|
|
415
|
+
it 'is expected to fetch the jwks from the url when the cache is empty' do
|
|
416
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
417
|
+
instance.jwks
|
|
418
|
+
|
|
419
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it 'is expected to fetch the jwks from the url when the cache is expired' do
|
|
423
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL, lifetime: 0)
|
|
424
|
+
instance.jwks
|
|
425
|
+
instance.jwks
|
|
426
|
+
|
|
427
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it 'is not expected to fetch the jwks from the url when there is a value cached' do
|
|
431
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
432
|
+
instance.jwks
|
|
433
|
+
instance.jwks
|
|
434
|
+
|
|
435
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it 'is expected to forcibly fetch the jwks from the url' do
|
|
439
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
440
|
+
instance.jwks
|
|
441
|
+
instance.jwks(force: true)
|
|
442
|
+
|
|
443
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'is expected to forcibly fetch the jwks from the url and cache it' do
|
|
447
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
448
|
+
instance.jwks(force: true)
|
|
449
|
+
instance.jwks
|
|
450
|
+
|
|
451
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
it 'is expected to return the last cached value if the jwks could not be fetched' do
|
|
455
|
+
Auth0::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
|
456
|
+
stub_request(:get, JWKS_URL).to_return(body: 'invalid')
|
|
457
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
458
|
+
|
|
459
|
+
expect(instance.jwks).to have_key('keys') and contain_exactly(a_hash_including(kid: 'test-key-1'))
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
it 'is expected to raise an error if the jwks could not be fetched and the cache is empty' do
|
|
463
|
+
stub_request(:get, JWKS_URL).to_return(body: 'invalid')
|
|
464
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
|
465
|
+
|
|
466
|
+
expect { instance.jwks }.to raise_exception('Could not fetch the JWK set')
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
# rubocop:enable Metrics/BlockLength
|
|
471
|
+
|
|
472
|
+
def stub_jwks(stub = JWKS_RESPONSE_1)
|
|
473
|
+
stub_request(:get, JWKS_URL).to_return(body: stub.to_json)
|
|
474
|
+
end
|