auth0 4.10.0 → 4.15.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/.env.example +2 -0
- data/CHANGELOG.md +55 -0
- data/DEPLOYMENT.md +14 -0
- data/Gemfile.lock +58 -53
- data/README.md +105 -25
- 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 +38 -2
- data/lib/auth0/api/v1/clients.rb +12 -2
- data/lib/auth0/api/v1/connections.rb +15 -0
- data/lib/auth0/api/v1/logs.rb +9 -0
- data/lib/auth0/api/v1/rules.rb +12 -0
- data/lib/auth0/api/v1/users.rb +63 -0
- data/lib/auth0/api/v2.rb +4 -0
- data/lib/auth0/api/v2/log_streams.rb +78 -0
- data/lib/auth0/api/v2/prompts.rb +70 -0
- data/lib/auth0/exception.rb +2 -0
- data/lib/auth0/mixins/httpproxy.rb +1 -1
- data/lib/auth0/mixins/validation.rb +307 -0
- data/lib/auth0/version.rb +1 -1
- data/spec/lib/auth0/api/authentication_endpoints_spec.rb +37 -10
- data/spec/lib/auth0/api/v2/log_streams_spec.rb +84 -0
- data/spec/lib/auth0/api/v2/prompts_spec.rb +88 -0
- data/spec/lib/auth0/mixins/validation_spec.rb +466 -0
- data/spec/spec_helper.rb +4 -0
- metadata +43 -5
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',
|
@@ -355,7 +354,8 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
355
354
|
expect(@instance).to receive(:post).with(
|
356
355
|
'/passwordless/start',
|
357
356
|
client_id: @instance.client_id,
|
358
|
-
|
357
|
+
client_secret: @instance.client_secret,
|
358
|
+
connection: 'email',
|
359
359
|
email: 'test@test.com',
|
360
360
|
send: 'code',
|
361
361
|
authParams: {
|
@@ -388,6 +388,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
388
388
|
expect(@instance).to receive(:post).with(
|
389
389
|
'/passwordless/start',
|
390
390
|
client_id: @instance.client_id,
|
391
|
+
client_secret: @instance.client_secret,
|
391
392
|
connection: 'sms',
|
392
393
|
phone_number: phone_number
|
393
394
|
)
|
@@ -550,7 +551,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
550
551
|
context '.userinfo' do
|
551
552
|
it { is_expected.to respond_to(:user_info) }
|
552
553
|
it 'is expected to make a GET request to /userinfo' do
|
553
|
-
is_expected.to receive(:get).with('/userinfo', {}, {'Authorization' => 'Bearer access-token'})
|
554
|
+
is_expected.to receive(:get).with('/userinfo', {}, { 'Authorization' => 'Bearer access-token' })
|
554
555
|
subject.userinfo 'access-token'
|
555
556
|
end
|
556
557
|
end
|
@@ -603,7 +604,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
603
604
|
|
604
605
|
it 'is expected to return a logout url with a client ID' do
|
605
606
|
expect(@instance.logout_url(return_to, include_client: true).to_s).to eq(
|
606
|
-
"https://#{@instance.domain}/v2/logout"
|
607
|
+
"https://#{@instance.domain}/v2/logout" \
|
607
608
|
"?returnTo=http%3A%2F%2Freturnto.com&client_id=#{@instance.client_id}"
|
608
609
|
)
|
609
610
|
end
|
@@ -653,7 +654,7 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
653
654
|
end
|
654
655
|
|
655
656
|
it 'is expected to get the wsfed url with wctx' do
|
656
|
-
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(
|
657
658
|
"https://#{@instance.domain}/wsfed/#{@instance.client_id}" \
|
658
659
|
"?whr=#{UP_AUTH}&wctx=wctx_test"
|
659
660
|
)
|
@@ -672,5 +673,31 @@ describe Auth0::Api::AuthenticationEndpoints do
|
|
672
673
|
)
|
673
674
|
end
|
674
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
|
675
702
|
end
|
676
703
|
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe Auth0::Api::V2::LogStreams do
|
3
|
+
before :all do
|
4
|
+
dummy_instance = DummyClass.new
|
5
|
+
dummy_instance.extend(Auth0::Api::V2::LogStreams)
|
6
|
+
dummy_instance.extend(Auth0::Mixins::Initializer)
|
7
|
+
@instance = dummy_instance
|
8
|
+
end
|
9
|
+
|
10
|
+
context '.log_streams' do
|
11
|
+
it { expect(@instance).to respond_to(:log_streams) }
|
12
|
+
it { expect(@instance).to respond_to(:get_log_streams) }
|
13
|
+
it 'is expected to call get /api/v2/log-streams' do
|
14
|
+
expect(@instance).to receive(:get).with(
|
15
|
+
'/api/v2/log-streams'
|
16
|
+
)
|
17
|
+
expect { @instance.log_streams }.not_to raise_error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context '.log_stream' do
|
22
|
+
it { expect(@instance).to respond_to(:log_stream) }
|
23
|
+
it 'is expected to call get /api/v2/log-streams/test' do
|
24
|
+
expect(@instance).to receive(:get).with('/api/v2/log-streams/test')
|
25
|
+
expect { @instance.log_stream('test') }.not_to raise_error
|
26
|
+
end
|
27
|
+
it 'expect to raise an error when calling with empty log stream id' do
|
28
|
+
expect { @instance.log_stream(nil) }.to raise_error 'Must supply a valid log stream id'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context '.create_log_stream' do
|
33
|
+
it { expect(@instance).to respond_to(:create_log_stream) }
|
34
|
+
it 'is expected to call post /api/v2/log-streams' do
|
35
|
+
expect(@instance).to receive(:post).with(
|
36
|
+
'/api/v2/log-streams',
|
37
|
+
name: 'test',
|
38
|
+
type: 'https',
|
39
|
+
sink: {
|
40
|
+
httpEndpoint: "https://mycompany.com",
|
41
|
+
httpContentType: "string",
|
42
|
+
httpContentFormat: "JSONLINES",
|
43
|
+
httpAuthorization: "string"
|
44
|
+
}
|
45
|
+
)
|
46
|
+
|
47
|
+
@instance.create_log_stream('test', 'https',
|
48
|
+
httpEndpoint: "https://mycompany.com",
|
49
|
+
httpContentType: "string",
|
50
|
+
httpContentFormat: "JSONLINES",
|
51
|
+
httpAuthorization: "string")
|
52
|
+
end
|
53
|
+
it 'expect to raise an error when calling with empty name' do
|
54
|
+
expect { @instance.create_log_stream('', '', '') }.to raise_error 'Name must contain at least one character'
|
55
|
+
end
|
56
|
+
it 'expect to raise an error when calling with empty type' do
|
57
|
+
expect { @instance.create_log_stream('name', '', '') }.to raise_error 'Must specify a valid type'
|
58
|
+
end
|
59
|
+
it 'expect to raise an error when calling without options' do
|
60
|
+
expect { @instance.create_log_stream('name', 'https', nil) }.to raise_error 'Must supply a valid hash for options'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context '.delete_log_stream' do
|
65
|
+
it { expect(@instance).to respond_to(:delete_log_stream) }
|
66
|
+
it 'is expected to call delete /api/v2/log-streams/test' do
|
67
|
+
expect(@instance).to receive(:delete).with('/api/v2/log-streams/test')
|
68
|
+
expect { @instance.delete_log_stream('test') }.not_to raise_error
|
69
|
+
end
|
70
|
+
it 'expect to raise an error when calling with empty log stream id' do
|
71
|
+
expect { @instance.delete_log_stream(nil) }.to raise_error 'Must supply a valid log stream id'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context '.patch_log_stream' do
|
76
|
+
it { expect(@instance).to respond_to(:patch_log_stream) }
|
77
|
+
it 'is expected to send patch to /api/v2/log_streams/test' do
|
78
|
+
expect(@instance).to receive(:patch).with('/api/v2/log-streams/test', status: 'paused')
|
79
|
+
expect { @instance.patch_log_stream('test', 'paused') }.not_to raise_error
|
80
|
+
end
|
81
|
+
it { expect { @instance.patch_log_stream('', nil) }.to raise_error 'Must specify a log stream id' }
|
82
|
+
it { expect { @instance.patch_log_stream('test', nil) }.to raise_error 'Must specify a valid status' }
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe Auth0::Api::V2::Prompts do
|
3
|
+
before :all do
|
4
|
+
dummy_instance = DummyClass.new
|
5
|
+
dummy_instance.extend(Auth0::Api::V2::Prompts)
|
6
|
+
@instance = dummy_instance
|
7
|
+
end
|
8
|
+
|
9
|
+
context '.prompts' do
|
10
|
+
it 'is expected to respond to a prompts method' do
|
11
|
+
expect(@instance).to respond_to(:prompts)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'is expected to respond to a get_prompts method' do
|
15
|
+
expect(@instance).to respond_to(:get_prompts)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'is expected to get /api/v2/prompts' do
|
19
|
+
expect(@instance).to receive(:get).with('/api/v2/prompts')
|
20
|
+
|
21
|
+
expect { @instance.prompts }.not_to raise_error
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context '.update_prompts' do
|
26
|
+
it 'is expected to respond to an update_prompts method' do
|
27
|
+
expect(@instance).to respond_to(:update_prompts)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'is expected to respond to a patch_prompts method' do
|
31
|
+
expect(@instance).to respond_to(:patch_prompts)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is expected to call patch /api/v2/prompts' do
|
35
|
+
expect(@instance).to receive(:patch).with('/api/v2/prompts', { universal_login_experience: 'new' })
|
36
|
+
|
37
|
+
expect { @instance.update_prompts(universal_login_experience: 'new') }.not_to raise_error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context '.custom_text' do
|
42
|
+
it 'is expected to respond to a custom_text method' do
|
43
|
+
expect(@instance).to respond_to(:custom_text)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'is expected to respond to a get_custom_text method' do
|
47
|
+
expect(@instance).to respond_to(:get_custom_text)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'is expected to get /api/v2/prompts/PROMPT_NAME/language/LANGUAGE' do
|
51
|
+
expect(@instance).to receive(:get).with('/api/v2/prompts/PROMPT_NAME/custom-text/LANGUAGE')
|
52
|
+
|
53
|
+
expect { @instance.custom_text('PROMPT_NAME', 'LANGUAGE') }.not_to raise_error
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'is expected to raise an exception when the prompt is empty' do
|
57
|
+
expect { @instance.custom_text(nil, 'language') }.to raise_exception(Auth0::InvalidParameter)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'is expected to raise an exception when the language is empty' do
|
61
|
+
expect { @instance.custom_text('prompt_name', nil) }.to raise_exception(Auth0::InvalidParameter)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context '.update_custom_text' do
|
66
|
+
it 'is expected to respond to an update_custom_text method' do
|
67
|
+
expect(@instance).to respond_to(:update_custom_text)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'is expected to respond to a put_custom_text method' do
|
71
|
+
expect(@instance).to respond_to(:put_custom_text)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'is expected to call put /api/v2/prompts/PROMPT_NAME/language/LANGUAGE' do
|
75
|
+
expect(@instance).to receive(:put).with('/api/v2/prompts/PROMPT_NAME/custom-text/LANGUAGE', 'BODY')
|
76
|
+
|
77
|
+
expect { @instance.update_custom_text('PROMPT_NAME', 'LANGUAGE', 'BODY') }.not_to raise_error
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'is expected to raise an exception when the prompt is empty' do
|
81
|
+
expect { @instance.update_custom_text(nil,'language', 'BODY') }.to raise_exception(Auth0::InvalidParameter)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'is expected to raise an exception when the language is empty' do
|
85
|
+
expect { @instance.update_custom_text('prompt_name', nil, 'BODY') }.to raise_exception(Auth0::InvalidParameter)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,466 @@
|
|
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 not to raise an error with a missing but not required nonce' do
|
233
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.-o5grnyODbBdRgzcrn7Sf9Hb6eOC0x_U2i3YjVgHN0U'
|
234
|
+
|
235
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'is expected to raise an error with a missing but required nonce' do
|
239
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.-o5grnyODbBdRgzcrn7Sf9Hb6eOC0x_U2i3YjVgHN0U'
|
240
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: 'a1b2c3d4e5' }))
|
241
|
+
|
242
|
+
expect { instance.validate(token) }.to raise_exception('Nonce (nonce) claim must be a string present in the ID token')
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'is expected to raise an error with an invalid nonce' do
|
246
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiMDAwOTk5IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.XqQPdFN4m5kmTUQQi_mAJu0LQOeUTS9lF2J_xWc_j-0'
|
247
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: 'a1b2c3d4e5' }))
|
248
|
+
|
249
|
+
expect { instance.validate(token) }.to raise_exception('Nonce (nonce) claim mismatch in the ID token; expected "a1b2c3d4e5", found "000999"')
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'is expected to raise an error with a missing azp' do
|
253
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.LrgYkIbWrxMq6jvvkL1lxWL237ii1IBhtN2o_tDxFns'
|
254
|
+
|
255
|
+
expect { @instance.validate(token) }.to raise_exception('Authorized Party (azp) claim must be a string present in the ID token')
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'is expected to raise an error with an invalid azp' do
|
259
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6ImV4dGVybmFsLXRlc3QtOTk5IiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.3DX-LY3B4UngDML-9nv11Sv89ECJpRpOLeWnkF1vAFY'
|
260
|
+
|
261
|
+
expect { @instance.validate(token) }.to raise_exception("Authorized Party (azp) claim mismatch in the ID token; expected \"tokens-test-123\", found \"external-test-999\"")
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'is expected to raise an error with a missing auth_time' do
|
265
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyJ9.JqUotnjHbGW0FcHz1s1YsRkce9Sbpsv2AEBDMpcUhp8'
|
266
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: 120 }))
|
267
|
+
|
268
|
+
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')
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'is expected to raise an error with a invalid auth_time' do
|
272
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzU5MjU2MX0.B7eWHJPHjhOh0ALjIQi0ro6zVsqGIeHd0gpRZsv6Hg8'
|
273
|
+
max_age = 120
|
274
|
+
auth_time = CLOCK + LEEWAY + max_age
|
275
|
+
clock = auth_time + 1
|
276
|
+
instance = Auth0::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: max_age, clock: clock }))
|
277
|
+
|
278
|
+
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}\"")
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe Auth0::Algorithm::HS256 do
|
284
|
+
context 'class' do
|
285
|
+
it 'is expected to respond to :secret' do
|
286
|
+
expect(Auth0::Algorithm::HS256).to respond_to(:secret)
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'is expected not to respond to :new' do
|
290
|
+
expect(Auth0::Algorithm::HS256).not_to respond_to(:new)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
context 'instance' do
|
295
|
+
it 'is expected to respond to :secret' do
|
296
|
+
instance = Auth0::Algorithm::HS256.secret('secret')
|
297
|
+
|
298
|
+
expect(instance).to respond_to(:secret)
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'is expected to return the secret' do
|
302
|
+
instance = Auth0::Algorithm::HS256.secret('secret')
|
303
|
+
|
304
|
+
expect(instance.secret).to eq('secret')
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'is expected to return the algorithm name' do
|
308
|
+
instance = Auth0::Algorithm::HS256.secret('secret')
|
309
|
+
|
310
|
+
expect(instance.name).to eq('HS256')
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
context 'parameters' do
|
315
|
+
expected_error = 'Must supply a valid secret'
|
316
|
+
|
317
|
+
it 'is expected to raise an error with a nil secret' do
|
318
|
+
expect { Auth0::Algorithm::HS256.secret(nil) }.to raise_exception(expected_error)
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'is expected to raise an error with an empty secret' do
|
322
|
+
expect { Auth0::Algorithm::HS256.secret('') }.to raise_exception(expected_error)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe Auth0::Algorithm::RS256 do
|
328
|
+
before :each do
|
329
|
+
stub_jwks
|
330
|
+
end
|
331
|
+
|
332
|
+
after :each do
|
333
|
+
Auth0::Algorithm::RS256.remove_jwks
|
334
|
+
WebMock.reset!
|
335
|
+
end
|
336
|
+
|
337
|
+
context 'class' do
|
338
|
+
it 'is expected to respond to :jwks_url' do
|
339
|
+
expect(Auth0::Algorithm::RS256).to respond_to(:jwks_url)
|
340
|
+
end
|
341
|
+
|
342
|
+
it 'is expected not to respond to :new' do
|
343
|
+
expect(Auth0::Algorithm::RS256).not_to respond_to(:new)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
context 'instance' do
|
348
|
+
it 'is expected to respond to :jwks' do
|
349
|
+
instance = Auth0::Algorithm::RS256.jwks_url('jwks url')
|
350
|
+
|
351
|
+
expect(instance).to respond_to(:jwks)
|
352
|
+
end
|
353
|
+
|
354
|
+
it 'is expected to respond to :fetched_jwks?' do
|
355
|
+
instance = Auth0::Algorithm::RS256.jwks_url('jwks url')
|
356
|
+
|
357
|
+
expect(instance).to respond_to(:fetched_jwks?)
|
358
|
+
end
|
359
|
+
|
360
|
+
it 'is expected to return a jwks' do
|
361
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
362
|
+
|
363
|
+
expect(instance.jwks).to have_key('keys') and contain_exactly(a_hash_including(kid: 'test-key-1'))
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'is expected to return if the jwks was fetched from the url' do
|
367
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
368
|
+
instance.jwks
|
369
|
+
|
370
|
+
expect(instance.fetched_jwks?).to eq(true)
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'is expected to return if the jwks was fetched from the cache' do
|
374
|
+
Auth0::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
375
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
376
|
+
instance.jwks
|
377
|
+
|
378
|
+
expect(instance.fetched_jwks?).to eq(false)
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'is expected to return the algorithm name' do
|
382
|
+
instance = Auth0::Algorithm::RS256.jwks_url('jwks url')
|
383
|
+
|
384
|
+
expect(instance.name).to eq('RS256')
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
context 'parameters' do
|
389
|
+
it 'is expected to raise an error with a nil jwks_url' do
|
390
|
+
expect { Auth0::Algorithm::RS256.jwks_url(nil) }.to raise_exception('Must supply a valid jwks_url')
|
391
|
+
end
|
392
|
+
|
393
|
+
it 'is expected to raise an error with an empty jwks_url' do
|
394
|
+
expect { Auth0::Algorithm::RS256.jwks_url('') }.to raise_exception('Must supply a valid jwks_url')
|
395
|
+
end
|
396
|
+
|
397
|
+
it 'is expected to raise an error with a non-integer lifetime' do
|
398
|
+
expect { Auth0::Algorithm::RS256.jwks_url('jwks url', lifetime: '1') }.to raise_exception('Must supply a valid lifetime')
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'is expected to raise an error with a negative lifetime' do
|
402
|
+
expect { Auth0::Algorithm::RS256.jwks_url('jwks url', lifetime: -1) }.to raise_exception('Must supply a valid lifetime')
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
context 'cache' do
|
407
|
+
it 'is expected to fetch the jwks from the url when the cache is empty' do
|
408
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
409
|
+
instance.jwks
|
410
|
+
|
411
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
412
|
+
end
|
413
|
+
|
414
|
+
it 'is expected to fetch the jwks from the url when the cache is expired' do
|
415
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL, lifetime: 0)
|
416
|
+
instance.jwks
|
417
|
+
instance.jwks
|
418
|
+
|
419
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
420
|
+
end
|
421
|
+
|
422
|
+
it 'is not expected to fetch the jwks from the url when there is a value cached' do
|
423
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
424
|
+
instance.jwks
|
425
|
+
instance.jwks
|
426
|
+
|
427
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
428
|
+
end
|
429
|
+
|
430
|
+
it 'is expected to forcibly fetch the jwks from the url' do
|
431
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
432
|
+
instance.jwks
|
433
|
+
instance.jwks(force: true)
|
434
|
+
|
435
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
436
|
+
end
|
437
|
+
|
438
|
+
it 'is expected to forcibly fetch the jwks from the url and cache it' do
|
439
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
440
|
+
instance.jwks(force: true)
|
441
|
+
instance.jwks
|
442
|
+
|
443
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
444
|
+
end
|
445
|
+
|
446
|
+
it 'is expected to return the last cached value if the jwks could not be fetched' do
|
447
|
+
Auth0::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
448
|
+
stub_request(:get, JWKS_URL).to_return(body: 'invalid')
|
449
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
450
|
+
|
451
|
+
expect(instance.jwks).to have_key('keys') and contain_exactly(a_hash_including(kid: 'test-key-1'))
|
452
|
+
end
|
453
|
+
|
454
|
+
it 'is expected to raise an error if the jwks could not be fetched and the cache is empty' do
|
455
|
+
stub_request(:get, JWKS_URL).to_return(body: 'invalid')
|
456
|
+
instance = Auth0::Algorithm::RS256.jwks_url(JWKS_URL)
|
457
|
+
|
458
|
+
expect { instance.jwks }.to raise_exception('Could not fetch the JWK set')
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
# rubocop:enable Metrics/BlockLength
|
463
|
+
|
464
|
+
def stub_jwks(stub = JWKS_RESPONSE_1)
|
465
|
+
stub_request(:get, JWKS_URL).to_return(body: stub.to_json)
|
466
|
+
end
|