minty 1.0.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 +7 -0
- data/.bundle/config +4 -0
- data/.devcontainer/Dockerfile +19 -0
- data/.devcontainer/devcontainer.json +37 -0
- data/.env.example +2 -0
- data/.gemrelease +2 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +33 -0
- data/.github/dependabot.yml +10 -0
- data/.github/stale.yml +20 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/CODE_OF_CONDUCT.md +3 -0
- data/DEPLOYMENT.md +61 -0
- data/DEVELOPMENT.md +35 -0
- data/Dockerfile +5 -0
- data/EXAMPLES.md +195 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +250 -0
- data/Guardfile +39 -0
- data/LICENSE +21 -0
- data/Makefile +5 -0
- data/README.md +88 -0
- data/RUBYGEM.md +9 -0
- data/Rakefile +33 -0
- data/codecov.yml +22 -0
- data/lib/minty/algorithm.rb +7 -0
- data/lib/minty/api/authentication_endpoints.rb +30 -0
- data/lib/minty/api/v2.rb +8 -0
- data/lib/minty/client.rb +7 -0
- data/lib/minty/exception.rb +50 -0
- data/lib/minty/mixins/api_token_struct.rb +4 -0
- data/lib/minty/mixins/headers.rb +19 -0
- data/lib/minty/mixins/httpproxy.rb +125 -0
- data/lib/minty/mixins/initializer.rb +38 -0
- data/lib/minty/mixins/validation.rb +113 -0
- data/lib/minty/mixins.rb +23 -0
- data/lib/minty/version.rb +5 -0
- data/lib/minty.rb +11 -0
- data/lib/minty_client.rb +4 -0
- data/minty.gemspec +39 -0
- data/publish_rubygem.sh +10 -0
- data/spec/integration/lib/minty/api/api_authentication_spec.rb +122 -0
- data/spec/integration/lib/minty/minty_client_spec.rb +92 -0
- data/spec/lib/minty/client_spec.rb +223 -0
- data/spec/lib/minty/mixins/httpproxy_spec.rb +658 -0
- data/spec/lib/minty/mixins/initializer_spec.rb +121 -0
- data/spec/lib/minty/mixins/token_management_spec.rb +129 -0
- data/spec/lib/minty/mixins/validation_spec.rb +559 -0
- data/spec/spec_helper.rb +75 -0
- data/spec/support/credentials.rb +14 -0
- data/spec/support/dummy_class.rb +20 -0
- data/spec/support/dummy_class_for_proxy.rb +6 -0
- data/spec/support/dummy_class_for_restclient.rb +4 -0
- data/spec/support/dummy_class_for_tokens.rb +18 -0
- data/spec/support/import_users.json +13 -0
- data/spec/support/stub_response.rb +3 -0
- metadata +366 -0
@@ -0,0 +1,559 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
RSA_PUB_KEY_JWK_1 = { 'kty': 'RSA', 'use': 'sig',
|
7
|
+
'n': 'uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw', 'e': 'AQAB', 'kid': 'test-key-1' }.freeze
|
8
|
+
RSA_PUB_KEY_JWK_2 = { 'kty': 'RSA', 'use': 'sig',
|
9
|
+
'n': 'uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw', 'e': 'AQAB', 'kid': 'test-key-2' }.freeze
|
10
|
+
JWKS_RESPONSE_1 = { 'keys': [RSA_PUB_KEY_JWK_1] }.freeze
|
11
|
+
JWKS_RESPONSE_2 = { 'keys': [RSA_PUB_KEY_JWK_2] }.freeze
|
12
|
+
JWKS_URL = 'https://tokens-test.minty.page/.well-known/jwks.json'
|
13
|
+
JWKS_URL_2 = 'https://tokens-test2.minty.page/.well-known/jwks.json'
|
14
|
+
HMAC_SHARED_SECRET = 'secret'
|
15
|
+
|
16
|
+
LEEWAY = 60
|
17
|
+
CLOCK = 1_587_592_561 # Apr 22 2020 21:56:01 UTC
|
18
|
+
CONTEXT = { algorithm: Minty::Algorithm::HS256.secret(HMAC_SHARED_SECRET), leeway: LEEWAY, audience: 'tokens-test-123',
|
19
|
+
issuer: 'https://tokens-test.minty.page/', clock: CLOCK }.freeze
|
20
|
+
|
21
|
+
describe Minty::Mixins::Validation::IdTokenValidator do
|
22
|
+
subject { @instance }
|
23
|
+
|
24
|
+
context 'instance' do
|
25
|
+
it 'is expected respond to :validate' do
|
26
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new({})
|
27
|
+
|
28
|
+
expect(instance).to respond_to(:validate)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'ID token decoding' do
|
33
|
+
expected_error = 'ID token could not be decoded'
|
34
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new({})
|
35
|
+
|
36
|
+
it 'is expected to raise an error with a nil id_token' do
|
37
|
+
expect { instance.validate(nil) }.to raise_exception(expected_error)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'is expected to raise an error with an empty id_token' do
|
41
|
+
expect { instance.validate('') }.to raise_exception(expected_error)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'is expected to raise an error with an invalid format' do
|
45
|
+
expect { instance.validate('a.b') }.to raise_exception(expected_error)
|
46
|
+
expect { instance.validate('a.b.') }.to raise_exception(expected_error)
|
47
|
+
expect { instance.validate('a.b.c.d') }.to raise_exception(expected_error)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'is expected to raise an error with an invalid encoding' do
|
51
|
+
expect { instance.validate('a.b.c') }.to raise_exception(expected_error)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'algorithm verification' do
|
56
|
+
token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Hn38QVtN_mWN0c-jOa-Fqq69kXpbBp0THsvE-CQ47Ps'
|
57
|
+
|
58
|
+
it 'is expected to raise an error with an unsupported algorithm' do
|
59
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new({ algorithm: 'ES256' })
|
60
|
+
|
61
|
+
expect { instance.validate(token) }.to raise_exception('Signature algorithm of "ES256" is not supported')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'is expected to raise an error when the algorithm does not match the alg header value' do
|
65
|
+
algorithm = Minty::Algorithm::HS256.secret(HMAC_SHARED_SECRET)
|
66
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new({ algorithm: algorithm })
|
67
|
+
|
68
|
+
expect do
|
69
|
+
instance.validate(token)
|
70
|
+
end.to raise_exception('Signature algorithm of "ES256" is not supported. Expected the ID token to be signed with "HS256"')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'HS256 signature verification' do
|
75
|
+
before :each do
|
76
|
+
algorithm = Minty::Algorithm::HS256.secret(HMAC_SHARED_SECRET)
|
77
|
+
@instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ algorithm: algorithm }))
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'is expected not to raise an error with a valid signature' do
|
81
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Hn38QVtN_mWN0c-jOa-Fqq69kXpbBp0THsvE-CQ47Ps'
|
82
|
+
|
83
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'is expected to raise an error with an invalid signature' do
|
87
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.invalidsignature'
|
88
|
+
|
89
|
+
expect { @instance.validate(token) }.to raise_exception('Invalid ID token signature')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'RS256 signature verification' do
|
94
|
+
before :each do
|
95
|
+
stub_jwks
|
96
|
+
algorithm = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
97
|
+
@instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ algorithm: algorithm }))
|
98
|
+
end
|
99
|
+
|
100
|
+
after :each do
|
101
|
+
Minty::Algorithm::RS256.remove_jwks
|
102
|
+
WebMock.reset!
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'is expected not to raise an error with a valid signature' do
|
106
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.jE00ARUiAwrKEoAMwbioKYj4bUZjmg31V7McDtIPsJJ16rYcvI-e5mtSSMgCmAom6t-WA7dsSWCJUlBCW2nAMvyCZ-hj8HG9Z0RmQEiwig9Fk22avoX94zdx65TwAeDfn2uMRaX_ps3TJcn4nymUtMp8Lps_vMw15eJerKThlSO4KuLTrvDDdRaCRamAd7jxuzhiwOt0mB0TVD55b5itA02pGuyapbjQXvvLYEx8OgpyIaAkB9Ry25abgjev0bSw2kjFZckG3lv9QgvZM85m9l3Rbrc6msNPGfMDFWGyT3Tu2ObqnSEA-57hZeuCbFrOya3vUwgSlc66rfvZj2xpzg'
|
107
|
+
|
108
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'is expected to raise an error with an invalid signature' do
|
112
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.invalidsignature'
|
113
|
+
|
114
|
+
expect { @instance.validate(token) }.to raise_exception('Invalid ID token signature')
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'is expected to raise an error when the public key cannot be found' do
|
118
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTIifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.r2ksgiiM8zGJ6byea_Fq_zWWEmUdOnwQLVdb5JzgdBv1GUQFp-4LNaRhcga4FIrbKgxaPeewGLTq2VqfjmNJUNfARcE3QEacQ_JEHkC6zKZIiqcu4msHl8X9HXyHxOPHMTTjYMjauPzET7UH_oLxF68DDDQqvKX9VqLsncpyC-KdTCFTLGlFSq6pxmYt6gwrF2Uo15Gzc6qe2I9-MTXCYd44VW1zQi6GhNJNKbXH6U3bf7nof0ot1PSjBXXuLgf6d3qumTStECCjIUmdBb6FiEX4SSRI4MgHWj4q0LyN28baRpYwYPhVnjzUxOP7OLjKiHs45ORBhuAWhrJnuR_uBQ'
|
119
|
+
|
120
|
+
expect do
|
121
|
+
@instance.validate(token)
|
122
|
+
end.to raise_exception('Could not find a public key for Key ID (kid) "test-key-2"')
|
123
|
+
end
|
124
|
+
|
125
|
+
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
|
126
|
+
token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTIifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.r2ksgiiM8zGJ6byea_Fq_zWWEmUdOnwQLVdb5JzgdBv1GUQFp-4LNaRhcga4FIrbKgxaPeewGLTq2VqfjmNJUNfARcE3QEacQ_JEHkC6zKZIiqcu4msHl8X9HXyHxOPHMTTjYMjauPzET7UH_oLxF68DDDQqvKX9VqLsncpyC-KdTCFTLGlFSq6pxmYt6gwrF2Uo15Gzc6qe2I9-MTXCYd44VW1zQi6GhNJNKbXH6U3bf7nof0ot1PSjBXXuLgf6d3qumTStECCjIUmdBb6FiEX4SSRI4MgHWj4q0LyN28baRpYwYPhVnjzUxOP7OLjKiHs45ORBhuAWhrJnuR_uBQ'
|
127
|
+
Minty::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
128
|
+
stub_jwks(JWKS_RESPONSE_2)
|
129
|
+
@instance.validate(token)
|
130
|
+
|
131
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'context validation' do
|
136
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.Hn38QVtN_mWN0c-jOa-Fqq69kXpbBp0THsvE-CQ47Ps'
|
137
|
+
|
138
|
+
it 'is expected to raise an error with a non-integer leeway' do
|
139
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ leeway: '1' }))
|
140
|
+
|
141
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid leeway')
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'is expected to raise an error with a negative leeway' do
|
145
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ leeway: -1 }))
|
146
|
+
|
147
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid leeway')
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'is expected to raise an error with an empty nonce' do
|
151
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: '' }))
|
152
|
+
|
153
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid nonce')
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'is expected to raise an error with an empty organization' do
|
157
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: '' }))
|
158
|
+
|
159
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid organization')
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'is expected to raise an error with an empty issuer' do
|
163
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ issuer: '' }))
|
164
|
+
|
165
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid issuer')
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'is expected to raise an error with an empty audience' do
|
169
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ audience: '' }))
|
170
|
+
|
171
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid audience')
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'is expected to raise an error with a non-integer max_age' do
|
175
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: '1' }))
|
176
|
+
|
177
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid max_age')
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'is expected to raise an error with a negative max_age' do
|
181
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: -1 }))
|
182
|
+
|
183
|
+
expect { instance.validate(token) }.to raise_exception('Must supply a valid max_age')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'claims validation' do
|
188
|
+
before :all do
|
189
|
+
@instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'is expected to raise an error with a missing iss' do
|
193
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.QL2B2tqJhlW9rc8HQ3PQKkjDufBeSvtRBtJmRPdQ5K8'
|
194
|
+
|
195
|
+
expect do
|
196
|
+
@instance.validate(token)
|
197
|
+
end.to raise_exception('Issuer (iss) claim must be a string present in the ID token')
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'is expected to raise an error with a invalid iss' do
|
201
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb21ldGhpbmctZWxzZSIsInN1YiI6ImF1dGgwfDEyMzQ1Njc4OSIsImF1ZCI6WyJ0b2tlbnMtdGVzdC0xMjMiLCJleHRlcm5hbC10ZXN0LTk5OSJdLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.AhMMouDlGMdxTYrY9Cn-p8svJ8ssKmsHeT6JNRVTh10'
|
202
|
+
|
203
|
+
expect do
|
204
|
+
@instance.validate(token)
|
205
|
+
end.to raise_exception("Issuer (iss) claim mismatch in the ID token; expected \"#{CONTEXT[:issuer]}\", found \"something-else\"")
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'is expected to raise an error with a missing sub' do
|
209
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0._4sUXtAZYpGrO3QaYArXnk2JivCqixa7hgHhH3w4SlY'
|
210
|
+
|
211
|
+
expect do
|
212
|
+
@instance.validate(token)
|
213
|
+
end.to raise_exception('Subject (sub) claim must be a string present in the ID token')
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'is expected to raise an error with a missing aud' do
|
217
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.TlwnBmGUKe0SElSYKxPqsG1mujkK2t1CwDJGGiWRdXA'
|
218
|
+
|
219
|
+
expect do
|
220
|
+
@instance.validate(token)
|
221
|
+
end.to raise_exception('Audience (aud) claim must be a string or array of strings present in the ID token')
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'is expected to raise an error with an invalid string aud' do
|
225
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOiJleHRlcm5hbC10ZXN0LTk5OSIsImV4cCI6MTU4Nzc2NTM2MSwiaWF0IjoxNTg3NTkyNTYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.-Tf5CIi2bZ51UdgqxFWQNXpJJmK5GgsetcVoVrQwHIA'
|
226
|
+
|
227
|
+
expect do
|
228
|
+
@instance.validate(token)
|
229
|
+
end.to raise_exception("Audience (aud) claim mismatch in the ID token; expected \"#{CONTEXT[:audience]}\", found \"external-test-999\"")
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'is expected to raise an error with an invalid array aud' do
|
233
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsiZXh0ZXJuYWwtdGVzdC05OTgiLCJleHRlcm5hbC10ZXN0LTk5OSJdLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MSwibm9uY2UiOiJhMWIyYzNkNGU1IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.Q1GRVL5g3RLQqG5sEV_cc8WW_oiZzFIAfzRfBdxMW2s'
|
234
|
+
|
235
|
+
expect do
|
236
|
+
@instance.validate(token)
|
237
|
+
end.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\"")
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'is expected to raise an error with a missing exp' do
|
241
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiaWF0IjoxNTg3NTkyNTYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.aoLiQX3sHsf1bEbc0axbjJ9qV6hhomtEzJq-FT8OGF0'
|
242
|
+
|
243
|
+
expect do
|
244
|
+
@instance.validate(token)
|
245
|
+
end.to raise_exception('Expiration Time (exp) claim must be a number present in the ID token')
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'is expected to raise an error with a invalid exp' do
|
249
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NTkyNTYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.A8Pc0vlCG5Ufez7VIoRqXTYpJehalTEgGX9cR2xJLkU'
|
250
|
+
clock = CLOCK + LEEWAY + 1
|
251
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ clock: clock }))
|
252
|
+
|
253
|
+
expect do
|
254
|
+
instance.validate(token)
|
255
|
+
end.to raise_exception("Expiration Time (exp) claim mismatch in the ID token; current time \"#{clock}\" is after expiration time \"1587592621\"")
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'is expected to raise an error with a missing iat' do
|
259
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJub25jZSI6ImExYjJjM2Q0ZTUiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1ODc2Nzg5NjF9.Jea6UVJsAK7Hnb494f_WIQCIbaLTnnCvMenSY1Y2toc'
|
260
|
+
|
261
|
+
expect do
|
262
|
+
@instance.validate(token)
|
263
|
+
end.to raise_exception('Issued At (iat) claim must be a number present in the ID token')
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'is expected not to raise an error with a missing but not required nonce' do
|
267
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.-o5grnyODbBdRgzcrn7Sf9Hb6eOC0x_U2i3YjVgHN0U'
|
268
|
+
|
269
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'is expected to raise an error with a missing but required nonce' do
|
273
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.-o5grnyODbBdRgzcrn7Sf9Hb6eOC0x_U2i3YjVgHN0U'
|
274
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: 'a1b2c3d4e5' }))
|
275
|
+
|
276
|
+
expect do
|
277
|
+
instance.validate(token)
|
278
|
+
end.to raise_exception('Nonce (nonce) claim must be a string present in the ID token')
|
279
|
+
end
|
280
|
+
|
281
|
+
it 'is expected to raise an error with an invalid nonce' do
|
282
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiMDAwOTk5IiwiYXpwIjoidG9rZW5zLXRlc3QtMTIzIiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.XqQPdFN4m5kmTUQQi_mAJu0LQOeUTS9lF2J_xWc_j-0'
|
283
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ nonce: 'a1b2c3d4e5' }))
|
284
|
+
|
285
|
+
expect do
|
286
|
+
instance.validate(token)
|
287
|
+
end.to raise_exception('Nonce (nonce) claim mismatch in the ID token; expected "a1b2c3d4e5", found "000999"')
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'is expected to raise an error with a missing azp' do
|
291
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF1dGhfdGltZSI6MTU4NzY3ODk2MX0.LrgYkIbWrxMq6jvvkL1lxWL237ii1IBhtN2o_tDxFns'
|
292
|
+
|
293
|
+
expect do
|
294
|
+
@instance.validate(token)
|
295
|
+
end.to raise_exception('Authorized Party (azp) claim must be a string present in the ID token')
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'is expected to raise an error with an invalid azp' do
|
299
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6ImV4dGVybmFsLXRlc3QtOTk5IiwiYXV0aF90aW1lIjoxNTg3Njc4OTYxfQ.3DX-LY3B4UngDML-9nv11Sv89ECJpRpOLeWnkF1vAFY'
|
300
|
+
|
301
|
+
expect do
|
302
|
+
@instance.validate(token)
|
303
|
+
end.to raise_exception('Authorized Party (azp) claim mismatch in the ID token; expected "tokens-test-123", found "external-test-999"')
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'is expected to raise an error with a missing auth_time' do
|
307
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyJ9.JqUotnjHbGW0FcHz1s1YsRkce9Sbpsv2AEBDMpcUhp8'
|
308
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: 120 }))
|
309
|
+
|
310
|
+
expect do
|
311
|
+
instance.validate(token)
|
312
|
+
end.to raise_exception('Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified')
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'is expected to raise an error with a invalid auth_time' do
|
316
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTU4NzU5MjU2MX0.B7eWHJPHjhOh0ALjIQi0ro6zVsqGIeHd0gpRZsv6Hg8'
|
317
|
+
max_age = 120
|
318
|
+
auth_time = CLOCK + LEEWAY + max_age
|
319
|
+
clock = auth_time + 1
|
320
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ max_age: max_age, clock: clock }))
|
321
|
+
|
322
|
+
expect do
|
323
|
+
instance.validate(token)
|
324
|
+
end.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}\"")
|
325
|
+
end
|
326
|
+
|
327
|
+
it 'is expected not to raise an error when org_id exsist in the token, but not required' do
|
328
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE3ODgxLCJpYXQiOjE2MTY0NDUwODEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTQ4MSwib3JnX2lkIjoidGVzdE9yZyJ9.AOafUKUNgaxUXpSRYFCeJERcwrQZ4q2NZlutwGXnh9I'
|
329
|
+
expect { @instance.validate(token) }.not_to raise_exception
|
330
|
+
end
|
331
|
+
|
332
|
+
it 'is expected to raise an error with a missing but required organization' do
|
333
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE4MTg1LCJpYXQiOjE2MTY0NDUzODUsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTc4NX0.UMo5pmgceXO9lIKzbk7X0ZhE5DOe0IP2LfMKdUj03zQ'
|
334
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'a1b2c3d4e5' }))
|
335
|
+
|
336
|
+
expect do
|
337
|
+
instance.validate(token)
|
338
|
+
end.to raise_exception('Organization Id (org_id) claim must be a string present in the ID token')
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'is expected to raise an error with an invalid organization' do
|
342
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE3ODgxLCJpYXQiOjE2MTY0NDUwODEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTQ4MSwib3JnX2lkIjoidGVzdE9yZyJ9.AOafUKUNgaxUXpSRYFCeJERcwrQZ4q2NZlutwGXnh9I'
|
343
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'a1b2c3d4e5' }))
|
344
|
+
|
345
|
+
expect do
|
346
|
+
instance.validate(token)
|
347
|
+
end.to raise_exception('Organization Id (org_id) claim value mismatch in the ID token; expected "a1b2c3d4e5", found "testOrg"')
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'is expected to NOT raise an error with a valid organization' do
|
351
|
+
token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC05OTkiXSwiZXhwIjoxNjE2NjE3ODgxLCJpYXQiOjE2MTY0NDUwODEsIm5vbmNlIjoiYTFiMmMzZDRlNSIsImF6cCI6InRva2Vucy10ZXN0LTEyMyIsImF1dGhfdGltZSI6MTYxNjUzMTQ4MSwib3JnX2lkIjoidGVzdE9yZyJ9.AOafUKUNgaxUXpSRYFCeJERcwrQZ4q2NZlutwGXnh9I'
|
352
|
+
instance = Minty::Mixins::Validation::IdTokenValidator.new(CONTEXT.merge({ organization: 'testOrg' }))
|
353
|
+
|
354
|
+
expect { instance.validate(token) }.not_to raise_exception
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
describe Minty::Algorithm::HS256 do
|
360
|
+
context 'class' do
|
361
|
+
it 'is expected to respond to :secret' do
|
362
|
+
expect(Minty::Algorithm::HS256).to respond_to(:secret)
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'is expected not to respond to :new' do
|
366
|
+
expect(Minty::Algorithm::HS256).not_to respond_to(:new)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
context 'instance' do
|
371
|
+
it 'is expected to respond to :secret' do
|
372
|
+
instance = Minty::Algorithm::HS256.secret('secret')
|
373
|
+
|
374
|
+
expect(instance).to respond_to(:secret)
|
375
|
+
end
|
376
|
+
|
377
|
+
it 'is expected to return the secret' do
|
378
|
+
instance = Minty::Algorithm::HS256.secret('secret')
|
379
|
+
|
380
|
+
expect(instance.secret).to eq('secret')
|
381
|
+
end
|
382
|
+
|
383
|
+
it 'is expected to return the algorithm name' do
|
384
|
+
instance = Minty::Algorithm::HS256.secret('secret')
|
385
|
+
|
386
|
+
expect(instance.name).to eq('HS256')
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
context 'parameters' do
|
391
|
+
expected_error = 'Must supply a valid secret'
|
392
|
+
|
393
|
+
it 'is expected to raise an error with a nil secret' do
|
394
|
+
expect { Minty::Algorithm::HS256.secret(nil) }.to raise_exception(expected_error)
|
395
|
+
end
|
396
|
+
|
397
|
+
it 'is expected to raise an error with an empty secret' do
|
398
|
+
expect { Minty::Algorithm::HS256.secret('') }.to raise_exception(expected_error)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
describe Minty::Algorithm::RS256 do
|
404
|
+
before :each do
|
405
|
+
stub_jwks
|
406
|
+
end
|
407
|
+
|
408
|
+
after :each do
|
409
|
+
Minty::Algorithm::RS256.remove_jwks
|
410
|
+
WebMock.reset!
|
411
|
+
end
|
412
|
+
|
413
|
+
context 'class' do
|
414
|
+
it 'is expected to respond to :jwks_url' do
|
415
|
+
expect(Minty::Algorithm::RS256).to respond_to(:jwks_url)
|
416
|
+
end
|
417
|
+
|
418
|
+
it 'is expected not to respond to :new' do
|
419
|
+
expect(Minty::Algorithm::RS256).not_to respond_to(:new)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
context 'instance' do
|
424
|
+
it 'is expected to respond to :jwks' do
|
425
|
+
instance = Minty::Algorithm::RS256.jwks_url('jwks url')
|
426
|
+
|
427
|
+
expect(instance).to respond_to(:jwks)
|
428
|
+
end
|
429
|
+
|
430
|
+
it 'is expected to respond to :fetched_jwks?' do
|
431
|
+
instance = Minty::Algorithm::RS256.jwks_url('jwks url')
|
432
|
+
|
433
|
+
expect(instance).to respond_to(:fetched_jwks?)
|
434
|
+
end
|
435
|
+
|
436
|
+
it 'is expected to return a jwks' do
|
437
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
438
|
+
|
439
|
+
expect(instance.jwks).to have_key('keys') and contain_exactly(a_hash_including(kid: 'test-key-1'))
|
440
|
+
end
|
441
|
+
|
442
|
+
it 'is expected to return if the jwks was fetched from the url' do
|
443
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
444
|
+
instance.jwks
|
445
|
+
|
446
|
+
expect(instance.fetched_jwks?).to eq(true)
|
447
|
+
end
|
448
|
+
|
449
|
+
it 'is expected to return if the jwks was fetched from the cache' do
|
450
|
+
Minty::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
451
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
452
|
+
instance.jwks
|
453
|
+
|
454
|
+
expect(instance.fetched_jwks?).to eq(false)
|
455
|
+
end
|
456
|
+
|
457
|
+
it 'is expected to return the algorithm name' do
|
458
|
+
instance = Minty::Algorithm::RS256.jwks_url('jwks url')
|
459
|
+
|
460
|
+
expect(instance.name).to eq('RS256')
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
context 'parameters' do
|
465
|
+
it 'is expected to raise an error with a nil jwks_url' do
|
466
|
+
expect { Minty::Algorithm::RS256.jwks_url(nil) }.to raise_exception('Must supply a valid jwks_url')
|
467
|
+
end
|
468
|
+
|
469
|
+
it 'is expected to raise an error with an empty jwks_url' do
|
470
|
+
expect { Minty::Algorithm::RS256.jwks_url('') }.to raise_exception('Must supply a valid jwks_url')
|
471
|
+
end
|
472
|
+
|
473
|
+
it 'is expected to raise an error with a non-integer lifetime' do
|
474
|
+
expect do
|
475
|
+
Minty::Algorithm::RS256.jwks_url('jwks url', lifetime: '1')
|
476
|
+
end.to raise_exception('Must supply a valid lifetime')
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'is expected to raise an error with a negative lifetime' do
|
480
|
+
expect do
|
481
|
+
Minty::Algorithm::RS256.jwks_url('jwks url', lifetime: -1)
|
482
|
+
end.to raise_exception('Must supply a valid lifetime')
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
context 'cache' do
|
487
|
+
it 'is expected to fetch the jwks from the url when the cache is empty' do
|
488
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
489
|
+
instance.jwks
|
490
|
+
|
491
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
492
|
+
end
|
493
|
+
|
494
|
+
it 'is expected to fetch the jwks from the url when the cache is expired' do
|
495
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL, lifetime: 0)
|
496
|
+
instance.jwks
|
497
|
+
instance.jwks
|
498
|
+
|
499
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
500
|
+
end
|
501
|
+
|
502
|
+
it 'is not expected to fetch the jwks from the url when there is a value cached' do
|
503
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
504
|
+
instance.jwks
|
505
|
+
instance.jwks
|
506
|
+
|
507
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
508
|
+
end
|
509
|
+
|
510
|
+
it 'is expected to fetch the jwks from multiple urls' do
|
511
|
+
stub_jwks(JWKS_RESPONSE_2, JWKS_URL_2)
|
512
|
+
|
513
|
+
instance1 = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
514
|
+
instance2 = Minty::Algorithm::RS256.jwks_url(JWKS_URL_2)
|
515
|
+
instance1.jwks
|
516
|
+
instance2.jwks
|
517
|
+
instance1.jwks
|
518
|
+
|
519
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
520
|
+
expect(a_request(:get, JWKS_URL_2)).to have_been_made.once
|
521
|
+
end
|
522
|
+
|
523
|
+
it 'is expected to forcibly fetch the jwks from the url' do
|
524
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
525
|
+
instance.jwks
|
526
|
+
instance.jwks(force: true)
|
527
|
+
|
528
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.twice
|
529
|
+
end
|
530
|
+
|
531
|
+
it 'is expected to forcibly fetch the jwks from the url and cache it' do
|
532
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
533
|
+
instance.jwks(force: true)
|
534
|
+
instance.jwks
|
535
|
+
|
536
|
+
expect(a_request(:get, JWKS_URL)).to have_been_made.once
|
537
|
+
end
|
538
|
+
|
539
|
+
it 'is expected to return the last cached value if the jwks could not be fetched' do
|
540
|
+
Minty::Algorithm::RS256.jwks_url(JWKS_URL).jwks
|
541
|
+
stub_request(:get, JWKS_URL).to_return(body: 'invalid')
|
542
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
543
|
+
|
544
|
+
expect(instance.jwks).to have_key('keys') and contain_exactly(a_hash_including(kid: 'test-key-1'))
|
545
|
+
end
|
546
|
+
|
547
|
+
it 'is expected to raise an error if the jwks could not be fetched and the cache is empty' do
|
548
|
+
stub_request(:get, JWKS_URL).to_return(body: 'invalid')
|
549
|
+
instance = Minty::Algorithm::RS256.jwks_url(JWKS_URL)
|
550
|
+
|
551
|
+
expect { instance.jwks }.to raise_exception('Could not fetch the JWK set')
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
# rubocop:enable Metrics/BlockLength
|
556
|
+
|
557
|
+
def stub_jwks(stub = JWKS_RESPONSE_1, url = JWKS_URL)
|
558
|
+
stub_request(:get, url).to_return(body: stub.to_json)
|
559
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pry'
|
4
|
+
require 'rack/test'
|
5
|
+
require 'faker'
|
6
|
+
require 'json'
|
7
|
+
require 'minty'
|
8
|
+
|
9
|
+
if RUBY_VERSION >= '2.7.2'
|
10
|
+
# NOTE: https://bugs.ruby-lang.org/issues/17000
|
11
|
+
Warning[:deprecated] = true
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'simplecov'
|
15
|
+
SimpleCov.start
|
16
|
+
|
17
|
+
if ENV['CI'] == 'true'
|
18
|
+
require 'simplecov-cobertura'
|
19
|
+
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'dotenv'
|
23
|
+
Dotenv.load
|
24
|
+
|
25
|
+
require 'webmock/rspec'
|
26
|
+
WebMock.allow_net_connect!
|
27
|
+
|
28
|
+
require 'vcr'
|
29
|
+
VCR.configure do |config|
|
30
|
+
# Uncomment the line below to record new VCR cassettes.
|
31
|
+
# When this is commented out, VCR will reject all outbound HTTP calls.
|
32
|
+
config.allow_http_connections_when_no_cassette = true
|
33
|
+
config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
|
34
|
+
config.configure_rspec_metadata!
|
35
|
+
config.hook_into :webmock
|
36
|
+
config.filter_sensitive_data('CLIENT_SECRET') { ENV['CLIENT_SECRET'] }
|
37
|
+
config.filter_sensitive_data('API_TOKEN') { ENV['MASTER_JWT'] }
|
38
|
+
|
39
|
+
ENV['DOMAIN'] = 'minty-sdk-tests.minty.page'
|
40
|
+
ENV['CLIENT_ID'] = '2cnWuug6zaFX1j0ge1P99jAUn0F4XSuI'
|
41
|
+
end
|
42
|
+
|
43
|
+
$LOAD_PATH.unshift File.expand_path(__dir__)
|
44
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
45
|
+
|
46
|
+
Dir['./lib/*.rb'].sort.each { |f| require f }
|
47
|
+
Dir['./lib/api/**/*.rb'].sort.each { |f| require f }
|
48
|
+
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
|
49
|
+
Dir['./spec/support/*.rb'].sort.each { |f| require f }
|
50
|
+
|
51
|
+
require 'rspec'
|
52
|
+
RSpec.configure do |config|
|
53
|
+
config.filter_run focus: true
|
54
|
+
config.run_all_when_everything_filtered = true
|
55
|
+
config.include Credentials
|
56
|
+
|
57
|
+
config.expect_with :rspec do |c|
|
58
|
+
c.max_formatted_output_length = 1_000_000
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def wait(time, increment = 5, elapsed_time = 0, &block)
|
63
|
+
yield
|
64
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
65
|
+
raise e if elapsed_time >= time
|
66
|
+
|
67
|
+
sleep increment
|
68
|
+
wait(time, increment, elapsed_time + increment, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def entity_suffix
|
72
|
+
'rubytest-210908'
|
73
|
+
end
|
74
|
+
|
75
|
+
puts "Entity suffix is #{entity_suffix}"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Credentials
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def v2_creds
|
7
|
+
{
|
8
|
+
domain: ENV.fetch('DOMAIN', 'DOMAIN'),
|
9
|
+
client_id: ENV.fetch('CLIENT_ID', 'CLIENT_ID'),
|
10
|
+
client_secret: ENV.fetch('CLIENT_SECRET', 'TEST_CLIENT_SECRET'),
|
11
|
+
token: ENV.fetch('MASTER_JWT', 'TEST_MASTER_JWT')
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DummyClass
|
4
|
+
include Minty::Mixins::Headers
|
5
|
+
|
6
|
+
attr_reader :domain, :client_id, :client_secret, :audience
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@domain = 'test.minty.page'
|
10
|
+
@client_id = '__test_client_id__'
|
11
|
+
@client_secret = '__test_client_secret__'
|
12
|
+
@audience = "https://#{@domain}/api/v2/"
|
13
|
+
end
|
14
|
+
|
15
|
+
%i[get post put patch delete delete_with_body].each do |method|
|
16
|
+
define_method(method) do |_path, _body = {}|
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|