omniauth-auth0 2.1.0 → 2.4.1
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/.circleci/config.yml +22 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- data/.github/stale.yml +20 -0
- data/.gitignore +1 -2
- data/.snyk +9 -0
- data/CHANGELOG.md +50 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +167 -0
- data/README.md +34 -17
- data/codecov.yml +22 -0
- data/lib/omniauth-auth0.rb +1 -1
- data/lib/omniauth-auth0/version.rb +1 -1
- data/lib/omniauth/auth0/errors.rb +11 -0
- data/lib/omniauth/auth0/jwt_validator.rb +158 -29
- data/lib/omniauth/auth0/telemetry.rb +36 -0
- data/lib/omniauth/strategies/auth0.rb +41 -17
- data/omniauth-auth0.gemspec +0 -2
- data/spec/omniauth/auth0/jwt_validator_spec.rb +450 -70
- data/spec/omniauth/auth0/telemetry_spec.rb +28 -0
- data/spec/omniauth/strategies/auth0_spec.rb +51 -1
- data/spec/spec_helper.rb +6 -4
- metadata +13 -5
- data/.travis.yml +0 -6
data/lib/omniauth-auth0.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require 'omniauth-auth0/version'
|
1
|
+
require 'omniauth-auth0/version'
|
2
2
|
require 'omniauth/strategies/auth0'
|
@@ -2,44 +2,71 @@ require 'base64'
|
|
2
2
|
require 'uri'
|
3
3
|
require 'json'
|
4
4
|
require 'omniauth'
|
5
|
+
require 'omniauth/auth0/errors'
|
5
6
|
|
6
7
|
module OmniAuth
|
7
8
|
module Auth0
|
8
9
|
# JWT Validator class
|
9
10
|
class JWTValidator
|
10
|
-
attr_accessor :issuer
|
11
|
+
attr_accessor :issuer, :domain
|
11
12
|
|
12
13
|
# Initializer
|
13
14
|
# @param options object
|
14
15
|
# options.domain - Application domain.
|
16
|
+
# options.issuer - Application issuer (optional).
|
15
17
|
# options.client_id - Application Client ID.
|
16
18
|
# options.client_secret - Application Client Secret.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
|
20
|
+
def initialize(options, authorize_params = {})
|
21
|
+
@domain = uri_string(options.domain)
|
22
|
+
|
23
|
+
# Use custom issuer if provided, otherwise use domain
|
24
|
+
@issuer = @domain
|
25
|
+
@issuer = uri_string(options.issuer) if options.respond_to?(:issuer)
|
21
26
|
|
22
27
|
@client_id = options.client_id
|
23
28
|
@client_secret = options.client_secret
|
24
29
|
end
|
25
30
|
|
26
|
-
#
|
27
|
-
# @
|
28
|
-
|
29
|
-
# @see https://github.com/jwt/ruby-jwt
|
30
|
-
def decode(jwt)
|
31
|
+
# Verify a token's signature. Only tokens signed with the RS256 or HS256 signatures are supported.
|
32
|
+
# @return array - The token's key and signing algorithm
|
33
|
+
def verify_signature(jwt)
|
31
34
|
head = token_head(jwt)
|
32
35
|
|
33
36
|
# Make sure the algorithm is supported and get the decode key.
|
34
|
-
decode_key = @client_secret
|
35
37
|
if head[:alg] == 'RS256'
|
36
|
-
|
37
|
-
elsif head[:alg]
|
38
|
-
|
38
|
+
key, alg = [rs256_decode_key(head[:kid]), head[:alg]]
|
39
|
+
elsif head[:alg] == 'HS256'
|
40
|
+
key, alg = [@client_secret, head[:alg]]
|
41
|
+
else
|
42
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Signature algorithm of #{head[:alg]} is not supported. Expected the ID token to be signed with RS256 or HS256")
|
39
43
|
end
|
40
44
|
|
41
|
-
#
|
42
|
-
JWT.decode(jwt,
|
45
|
+
# Call decode to verify the signature
|
46
|
+
JWT.decode(jwt, key, true, decode_opts(alg))
|
47
|
+
|
48
|
+
return key, alg
|
49
|
+
end
|
50
|
+
|
51
|
+
# Verify a JWT.
|
52
|
+
# @param jwt string - JWT to verify.
|
53
|
+
# @param authorize_params hash - Authorization params to verify on the JWT
|
54
|
+
# @return hash - The verified token, if there were no exceptions.
|
55
|
+
def verify(jwt, authorize_params = {})
|
56
|
+
if !jwt
|
57
|
+
raise OmniAuth::Auth0::TokenValidationError.new('ID token is required but missing')
|
58
|
+
end
|
59
|
+
|
60
|
+
parts = jwt.split('.')
|
61
|
+
if parts.length != 3
|
62
|
+
raise OmniAuth::Auth0::TokenValidationError.new('ID token could not be decoded')
|
63
|
+
end
|
64
|
+
|
65
|
+
key, alg = verify_signature(jwt)
|
66
|
+
id_token, header = JWT.decode(jwt, key, false)
|
67
|
+
verify_claims(id_token, authorize_params)
|
68
|
+
|
69
|
+
return id_token
|
43
70
|
end
|
44
71
|
|
45
72
|
# Get the decoded head segment from a JWT.
|
@@ -73,34 +100,36 @@ module OmniAuth
|
|
73
100
|
end
|
74
101
|
|
75
102
|
private
|
76
|
-
|
77
|
-
#
|
78
|
-
# Docs: https://github.com/jwt/ruby-jwt#add-custom-header-fields
|
103
|
+
# Get the JWT decode options. We disable the claim checks since we perform our claim validation logic
|
104
|
+
# Docs: https://github.com/jwt/ruby-jwt
|
79
105
|
# @return hash
|
80
106
|
def decode_opts(alg)
|
81
107
|
{
|
82
108
|
algorithm: alg,
|
83
|
-
|
84
|
-
|
85
|
-
verify_iss:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
verify_not_before:
|
109
|
+
verify_expiration: false,
|
110
|
+
verify_iat: false,
|
111
|
+
verify_iss: false,
|
112
|
+
verify_aud: false,
|
113
|
+
verify_jti: false,
|
114
|
+
verify_subj: false,
|
115
|
+
verify_not_before: false
|
90
116
|
}
|
91
117
|
end
|
92
118
|
|
93
119
|
def rs256_decode_key(kid)
|
94
120
|
jwks_x5c = jwks_key(:x5c, kid)
|
95
|
-
|
121
|
+
|
122
|
+
if jwks_x5c.nil?
|
123
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}'")
|
124
|
+
end
|
96
125
|
|
97
126
|
jwks_public_cert(jwks_x5c.first)
|
98
127
|
end
|
99
128
|
|
100
|
-
# Get a JWKS from the
|
129
|
+
# Get a JWKS from the domain
|
101
130
|
# @return void
|
102
131
|
def jwks
|
103
|
-
jwks_uri = URI(@
|
132
|
+
jwks_uri = URI(@domain + '.well-known/jwks.json')
|
104
133
|
@jwks ||= json_parse(Net::HTTP.get(jwks_uri))
|
105
134
|
end
|
106
135
|
|
@@ -117,6 +146,106 @@ module OmniAuth
|
|
117
146
|
def json_parse(json)
|
118
147
|
JSON.parse(json, symbolize_names: true)
|
119
148
|
end
|
149
|
+
|
150
|
+
# Parse a URI into the desired string format
|
151
|
+
# @param uri - the URI to parse
|
152
|
+
# @return string
|
153
|
+
def uri_string(uri)
|
154
|
+
temp_domain = URI(uri)
|
155
|
+
temp_domain = URI("https://#{uri}") unless temp_domain.scheme
|
156
|
+
"#{temp_domain}/"
|
157
|
+
end
|
158
|
+
|
159
|
+
def verify_claims(id_token, authorize_params)
|
160
|
+
leeway = authorize_params[:leeway] || 60
|
161
|
+
max_age = authorize_params[:max_age]
|
162
|
+
nonce = authorize_params[:nonce]
|
163
|
+
|
164
|
+
verify_iss(id_token)
|
165
|
+
verify_sub(id_token)
|
166
|
+
verify_aud(id_token)
|
167
|
+
verify_expiration(id_token, leeway)
|
168
|
+
verify_iat(id_token)
|
169
|
+
verify_nonce(id_token, nonce)
|
170
|
+
verify_azp(id_token)
|
171
|
+
verify_auth_time(id_token, leeway, max_age)
|
172
|
+
end
|
173
|
+
|
174
|
+
def verify_iss(id_token)
|
175
|
+
issuer = id_token['iss']
|
176
|
+
if !issuer
|
177
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim must be a string present in the ID token")
|
178
|
+
elsif @issuer != issuer
|
179
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim mismatch in the ID token, expected (#{@issuer}), found (#{id_token['iss']})")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def verify_sub(id_token)
|
184
|
+
subject = id_token['sub']
|
185
|
+
if !subject || !subject.is_a?(String) || subject.empty?
|
186
|
+
raise OmniAuth::Auth0::TokenValidationError.new('Subject (sub) claim must be a string present in the ID token')
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def verify_aud(id_token)
|
191
|
+
audience = id_token['aud']
|
192
|
+
if !audience || !(audience.is_a?(String) || audience.is_a?(Array))
|
193
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim must be a string or array of strings present in the ID token")
|
194
|
+
elsif audience.is_a?(Array) && !audience.include?(@client_id)
|
195
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but was not one of #{audience.join(', ')}")
|
196
|
+
elsif audience.is_a?(String) && audience != @client_id
|
197
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but found #{audience}")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def verify_expiration(id_token, leeway)
|
202
|
+
expiration = id_token['exp']
|
203
|
+
if !expiration || !expiration.is_a?(Integer)
|
204
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim must be a number present in the ID token")
|
205
|
+
elsif expiration <= Time.now.to_i - leeway
|
206
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(expiration + leeway)})")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def verify_iat(id_token)
|
211
|
+
if !id_token['iat']
|
212
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Issued At (iat) claim must be a number present in the ID token")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def verify_nonce(id_token, nonce)
|
217
|
+
if nonce
|
218
|
+
received_nonce = id_token['nonce']
|
219
|
+
if !received_nonce
|
220
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim must be a string present in the ID token")
|
221
|
+
elsif nonce != received_nonce
|
222
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim value mismatch in the ID token; expected (#{nonce}), found (#{received_nonce})")
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def verify_azp(id_token)
|
228
|
+
audience = id_token['aud']
|
229
|
+
if audience.is_a?(Array) && audience.length > 1
|
230
|
+
azp = id_token['azp']
|
231
|
+
if !azp || !azp.is_a?(String)
|
232
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values")
|
233
|
+
elsif azp != @client_id
|
234
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim mismatch in the ID token; expected (#{@client_id}), found (#{azp})")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def verify_auth_time(id_token, leeway, max_age)
|
240
|
+
if max_age
|
241
|
+
auth_time = id_token['auth_time']
|
242
|
+
if !auth_time || !auth_time.is_a?(Integer)
|
243
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified")
|
244
|
+
elsif Time.now.to_i > auth_time + max_age + leeway;
|
245
|
+
raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + leeway)})")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
120
249
|
end
|
121
250
|
end
|
122
251
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module OmniAuth
|
4
|
+
module Auth0
|
5
|
+
# Module to provide necessary telemetry for API requests.
|
6
|
+
module Telemetry
|
7
|
+
|
8
|
+
# Return a telemetry hash to be encoded and sent to Auth0.
|
9
|
+
# @return hash
|
10
|
+
def telemetry
|
11
|
+
telemetry = {
|
12
|
+
name: 'omniauth-auth0',
|
13
|
+
version: OmniAuth::Auth0::VERSION,
|
14
|
+
env: {
|
15
|
+
ruby: RUBY_VERSION
|
16
|
+
}
|
17
|
+
}
|
18
|
+
add_rails_version telemetry
|
19
|
+
end
|
20
|
+
|
21
|
+
# JSON-ify and base64 encode the current telemetry.
|
22
|
+
# @return string
|
23
|
+
def telemetry_encoded
|
24
|
+
Base64.urlsafe_encode64(JSON.dump(telemetry))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def add_rails_version(telemetry)
|
30
|
+
return telemetry unless Gem.loaded_specs['rails'].respond_to? :version
|
31
|
+
telemetry[:env][:rails] = Gem.loaded_specs['rails'].version.to_s
|
32
|
+
telemetry
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,12 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
require 'uri'
|
5
|
+
require 'securerandom'
|
3
6
|
require 'omniauth-oauth2'
|
4
7
|
require 'omniauth/auth0/jwt_validator'
|
8
|
+
require 'omniauth/auth0/telemetry'
|
9
|
+
require 'omniauth/auth0/errors'
|
5
10
|
|
6
11
|
module OmniAuth
|
7
12
|
module Strategies
|
8
13
|
# Auth0 OmniAuth strategy
|
9
14
|
class Auth0 < OmniAuth::Strategies::OAuth2
|
15
|
+
include OmniAuth::Auth0::Telemetry
|
16
|
+
|
10
17
|
option :name, 'auth0'
|
11
18
|
|
12
19
|
args %i[
|
@@ -43,10 +50,16 @@ module OmniAuth
|
|
43
50
|
)
|
44
51
|
end
|
45
52
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
# Retrieve and remove authorization params from the session
|
54
|
+
session_authorize_params = session['authorize_params'] || {}
|
55
|
+
session.delete('authorize_params')
|
56
|
+
|
57
|
+
auth_scope = session_authorize_params[:scope]
|
58
|
+
if auth_scope.respond_to?(:include?) && auth_scope.include?('openid')
|
59
|
+
# Make sure the ID token can be verified and decoded.
|
60
|
+
auth0_jwt = OmniAuth::Auth0::JWTValidator.new(options)
|
61
|
+
auth0_jwt.verify(credentials['id_token'], session_authorize_params)
|
62
|
+
end
|
50
63
|
|
51
64
|
credentials
|
52
65
|
end
|
@@ -72,13 +85,27 @@ module OmniAuth
|
|
72
85
|
# Define the parameters used for the /authorize endpoint
|
73
86
|
def authorize_params
|
74
87
|
params = super
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
88
|
+
parsed_query = Rack::Utils.parse_query(request.query_string)
|
89
|
+
%w[connection connection_scope prompt screen_hint].each do |key|
|
90
|
+
params[key] = parsed_query[key] if parsed_query.key?(key)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Generate nonce
|
94
|
+
params[:nonce] = SecureRandom.hex
|
95
|
+
# Generate leeway if none exists
|
96
|
+
params[:leeway] = 60 unless params[:leeway]
|
97
|
+
|
98
|
+
# Store authorize params in the session for token verification
|
99
|
+
session['authorize_params'] = params
|
100
|
+
|
79
101
|
params
|
80
102
|
end
|
81
103
|
|
104
|
+
def build_access_token
|
105
|
+
options.token_params[:headers] = { 'Auth0-Client' => telemetry_encoded }
|
106
|
+
super
|
107
|
+
end
|
108
|
+
|
82
109
|
# Declarative override for the request phase of authentication
|
83
110
|
def request_phase
|
84
111
|
if no_client_id?
|
@@ -96,6 +123,12 @@ module OmniAuth
|
|
96
123
|
end
|
97
124
|
end
|
98
125
|
|
126
|
+
def callback_phase
|
127
|
+
super
|
128
|
+
rescue OmniAuth::Auth0::TokenValidationError => e
|
129
|
+
fail!(:token_validation_error, e)
|
130
|
+
end
|
131
|
+
|
99
132
|
private
|
100
133
|
|
101
134
|
# Parse the raw user info.
|
@@ -125,15 +158,6 @@ module OmniAuth
|
|
125
158
|
domain_url = URI("https://#{domain_url}") if domain_url.scheme.nil?
|
126
159
|
domain_url.to_s
|
127
160
|
end
|
128
|
-
|
129
|
-
# Build the auth0Client URL parameter for metrics.
|
130
|
-
def client_info
|
131
|
-
client_info = JSON.dump(
|
132
|
-
name: 'omniauth-auth0',
|
133
|
-
version: OmniAuth::Auth0::VERSION
|
134
|
-
)
|
135
|
-
Base64.urlsafe_encode64(client_info)
|
136
|
-
end
|
137
161
|
end
|
138
162
|
end
|
139
163
|
end
|
data/omniauth-auth0.gemspec
CHANGED
@@ -16,8 +16,6 @@ OmniAuth is a library that standardizes multi-provider authentication for web ap
|
|
16
16
|
omniauth-auth0 is the OmniAuth strategy for Auth0.
|
17
17
|
}
|
18
18
|
|
19
|
-
s.rubyforge_project = 'omniauth-auth0'
|
20
|
-
|
21
19
|
s.files = `git ls-files`.split("\n")
|
22
20
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
21
|
s.executables = `git ls-files -- bin/*`.split('\n').map{ |f| File.basename(f) }
|
@@ -12,17 +12,17 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
12
12
|
let(:domain) { 'samples.auth0.com' }
|
13
13
|
let(:future_timecode) { 32_503_680_000 }
|
14
14
|
let(:past_timecode) { 303_912_000 }
|
15
|
-
let(:
|
15
|
+
let(:valid_jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
|
16
16
|
|
17
17
|
let(:rsa_private_key) do
|
18
18
|
OpenSSL::PKey::RSA.generate 2048
|
19
19
|
end
|
20
20
|
|
21
|
-
let(:
|
21
|
+
let(:valid_jwks) do
|
22
22
|
{
|
23
23
|
keys: [
|
24
24
|
{
|
25
|
-
kid:
|
25
|
+
kid: valid_jwks_kid,
|
26
26
|
x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)]
|
27
27
|
}
|
28
28
|
]
|
@@ -35,8 +35,6 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
35
35
|
JSON.parse(jwks_file, symbolize_names: true)
|
36
36
|
end
|
37
37
|
|
38
|
-
Options = Struct.new(:domain, :client_id, :client_secret)
|
39
|
-
|
40
38
|
#
|
41
39
|
# Specs
|
42
40
|
#
|
@@ -93,56 +91,94 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
93
91
|
end
|
94
92
|
end
|
95
93
|
|
96
|
-
describe 'JWT verifier
|
94
|
+
describe 'JWT verifier jwks key parsing' do
|
97
95
|
let(:jwt_validator) do
|
98
96
|
make_jwt_validator
|
99
97
|
end
|
100
98
|
|
101
99
|
before do
|
102
|
-
|
100
|
+
stub_complete_jwks
|
103
101
|
end
|
104
102
|
|
105
103
|
it 'should return a key' do
|
106
|
-
expect(jwt_validator.jwks_key(:alg,
|
104
|
+
expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256')
|
107
105
|
end
|
108
106
|
|
109
107
|
it 'should return an x5c key' do
|
110
|
-
expect(jwt_validator.jwks_key(:x5c,
|
108
|
+
expect(jwt_validator.jwks_key(:x5c, valid_jwks_kid).length).to eq(1)
|
111
109
|
end
|
112
110
|
|
113
111
|
it 'should return nil if there is not key' do
|
114
|
-
expect(jwt_validator.jwks_key(:auth0,
|
112
|
+
expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil)
|
115
113
|
end
|
116
114
|
|
117
115
|
it 'should return nil if the key ID is invalid' do
|
118
|
-
expect(jwt_validator.jwks_key(:alg, "#{
|
116
|
+
expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe 'JWT verifier custom issuer' do
|
121
|
+
context 'same as domain' do
|
122
|
+
let(:jwt_validator) do
|
123
|
+
make_jwt_validator(opt_issuer: domain)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should have the correct issuer' do
|
127
|
+
expect(jwt_validator.issuer).to eq('https://samples.auth0.com/')
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should have the correct domain' do
|
131
|
+
expect(jwt_validator.issuer).to eq('https://samples.auth0.com/')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'different from domain' do
|
136
|
+
let(:jwt_validator) do
|
137
|
+
make_jwt_validator(opt_issuer: 'different.auth0.com')
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should have the correct issuer' do
|
141
|
+
expect(jwt_validator.issuer).to eq('https://different.auth0.com/')
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should have the correct domain' do
|
145
|
+
expect(jwt_validator.domain).to eq('https://samples.auth0.com/')
|
146
|
+
end
|
119
147
|
end
|
120
148
|
end
|
121
149
|
|
122
|
-
describe 'JWT verifier
|
150
|
+
describe 'JWT verifier verify' do
|
123
151
|
let(:jwt_validator) do
|
124
152
|
make_jwt_validator
|
125
153
|
end
|
126
154
|
|
127
155
|
before do
|
128
|
-
|
129
|
-
|
156
|
+
stub_complete_jwks
|
157
|
+
stub_expected_jwks
|
130
158
|
end
|
131
159
|
|
132
|
-
it 'should fail
|
133
|
-
payload = {
|
134
|
-
exp: past_timecode
|
135
|
-
}
|
136
|
-
token = make_hs256_token(payload)
|
160
|
+
it 'should fail when JWT is nil' do
|
137
161
|
expect do
|
138
|
-
jwt_validator.
|
139
|
-
end.to raise_error(
|
162
|
+
jwt_validator.verify(nil)
|
163
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
164
|
+
message: "ID token is required but missing"
|
165
|
+
}))
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should fail when JWT is not well-formed' do
|
169
|
+
expect do
|
170
|
+
jwt_validator.verify('abc.123')
|
171
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
172
|
+
message: "ID token could not be decoded"
|
173
|
+
}))
|
140
174
|
end
|
141
175
|
|
142
176
|
it 'should fail with missing issuer' do
|
143
177
|
expect do
|
144
|
-
jwt_validator.
|
145
|
-
end.to raise_error(
|
178
|
+
jwt_validator.verify(make_hs256_token)
|
179
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
180
|
+
message: "Issuer (iss) claim must be a string present in the ID token"
|
181
|
+
}))
|
146
182
|
end
|
147
183
|
|
148
184
|
it 'should fail with invalid issuer' do
|
@@ -151,55 +187,287 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
151
187
|
}
|
152
188
|
token = make_hs256_token(payload)
|
153
189
|
expect do
|
154
|
-
jwt_validator.
|
155
|
-
end.to raise_error(
|
190
|
+
jwt_validator.verify(token)
|
191
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
192
|
+
message: "Issuer (iss) claim mismatch in the ID token, expected (https://samples.auth0.com/), found (https://auth0.com/)"
|
193
|
+
}))
|
156
194
|
end
|
157
195
|
|
158
|
-
it 'should fail
|
196
|
+
it 'should fail when subject is missing' do
|
159
197
|
payload = {
|
160
|
-
|
161
|
-
|
198
|
+
iss: "https://#{domain}/",
|
199
|
+
sub: ''
|
162
200
|
}
|
163
201
|
token = make_hs256_token(payload)
|
164
202
|
expect do
|
165
|
-
jwt_validator.
|
166
|
-
end.to raise_error(
|
203
|
+
jwt_validator.verify(token)
|
204
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
205
|
+
message: "Subject (sub) claim must be a string present in the ID token"
|
206
|
+
}))
|
167
207
|
end
|
168
208
|
|
169
209
|
it 'should fail with missing audience' do
|
170
210
|
payload = {
|
171
|
-
iss: "https://#{domain}/"
|
211
|
+
iss: "https://#{domain}/",
|
212
|
+
sub: 'sub'
|
172
213
|
}
|
173
214
|
token = make_hs256_token(payload)
|
174
215
|
expect do
|
175
|
-
jwt_validator.
|
176
|
-
end.to raise_error(
|
216
|
+
jwt_validator.verify(token)
|
217
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
218
|
+
message: "Audience (aud) claim must be a string or array of strings present in the ID token"
|
219
|
+
}))
|
177
220
|
end
|
178
221
|
|
179
222
|
it 'should fail with invalid audience' do
|
180
223
|
payload = {
|
181
224
|
iss: "https://#{domain}/",
|
225
|
+
sub: 'sub',
|
182
226
|
aud: 'Auth0'
|
183
227
|
}
|
184
228
|
token = make_hs256_token(payload)
|
185
229
|
expect do
|
186
|
-
jwt_validator.
|
187
|
-
end.to raise_error(
|
230
|
+
jwt_validator.verify(token)
|
231
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
232
|
+
message: "Audience (aud) claim mismatch in the ID token; expected #{client_id} but found Auth0"
|
233
|
+
}))
|
188
234
|
end
|
189
235
|
|
190
|
-
it 'should
|
236
|
+
it 'should fail when missing expiration' do
|
191
237
|
payload = {
|
192
238
|
iss: "https://#{domain}/",
|
193
|
-
|
194
|
-
|
195
|
-
"https://#{domain}/userinfo"
|
196
|
-
]
|
239
|
+
sub: 'sub',
|
240
|
+
aud: client_id
|
197
241
|
}
|
242
|
+
|
198
243
|
token = make_hs256_token(payload)
|
199
|
-
expect
|
244
|
+
expect do
|
245
|
+
jwt_validator.verify(token)
|
246
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
247
|
+
message: "Expiration time (exp) claim must be a number present in the ID token"
|
248
|
+
}))
|
200
249
|
end
|
201
250
|
|
202
|
-
it 'should
|
251
|
+
it 'should fail when past expiration' do
|
252
|
+
payload = {
|
253
|
+
iss: "https://#{domain}/",
|
254
|
+
sub: 'sub',
|
255
|
+
aud: client_id,
|
256
|
+
exp: past_timecode
|
257
|
+
}
|
258
|
+
|
259
|
+
token = make_hs256_token(payload)
|
260
|
+
expect do
|
261
|
+
jwt_validator.verify(token)
|
262
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
263
|
+
message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(past_timecode + 60)})"
|
264
|
+
}))
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'should pass when past expiration but within default leeway' do
|
268
|
+
exp = Time.now.to_i - 59
|
269
|
+
payload = {
|
270
|
+
iss: "https://#{domain}/",
|
271
|
+
sub: 'sub',
|
272
|
+
aud: client_id,
|
273
|
+
exp: exp,
|
274
|
+
iat: past_timecode
|
275
|
+
}
|
276
|
+
|
277
|
+
token = make_hs256_token(payload)
|
278
|
+
id_token = jwt_validator.verify(token)
|
279
|
+
expect(id_token['exp']).to eq(exp)
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'should fail when past expiration and outside default leeway' do
|
283
|
+
exp = Time.now.to_i - 61
|
284
|
+
payload = {
|
285
|
+
iss: "https://#{domain}/",
|
286
|
+
sub: 'sub',
|
287
|
+
aud: client_id,
|
288
|
+
exp: exp,
|
289
|
+
iat: past_timecode
|
290
|
+
}
|
291
|
+
|
292
|
+
token = make_hs256_token(payload)
|
293
|
+
expect do
|
294
|
+
jwt_validator.verify(token)
|
295
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
296
|
+
message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(exp + 60)})"
|
297
|
+
}))
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'should fail when missing iat' do
|
301
|
+
payload = {
|
302
|
+
iss: "https://#{domain}/",
|
303
|
+
sub: 'sub',
|
304
|
+
aud: client_id,
|
305
|
+
exp: future_timecode
|
306
|
+
}
|
307
|
+
|
308
|
+
token = make_hs256_token(payload)
|
309
|
+
expect do
|
310
|
+
jwt_validator.verify(token)
|
311
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
312
|
+
message: "Issued At (iat) claim must be a number present in the ID token"
|
313
|
+
}))
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'should fail when authorize params has nonce but nonce is missing in the token' do
|
317
|
+
payload = {
|
318
|
+
iss: "https://#{domain}/",
|
319
|
+
sub: 'sub',
|
320
|
+
aud: client_id,
|
321
|
+
exp: future_timecode,
|
322
|
+
iat: past_timecode
|
323
|
+
}
|
324
|
+
|
325
|
+
token = make_hs256_token(payload)
|
326
|
+
expect do
|
327
|
+
jwt_validator.verify(token, { nonce: 'noncey' })
|
328
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
329
|
+
message: "Nonce (nonce) claim must be a string present in the ID token"
|
330
|
+
}))
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'should fail when authorize params has nonce but token nonce does not match' do
|
334
|
+
payload = {
|
335
|
+
iss: "https://#{domain}/",
|
336
|
+
sub: 'sub',
|
337
|
+
aud: client_id,
|
338
|
+
exp: future_timecode,
|
339
|
+
iat: past_timecode,
|
340
|
+
nonce: 'mismatch'
|
341
|
+
}
|
342
|
+
|
343
|
+
token = make_hs256_token(payload)
|
344
|
+
expect do
|
345
|
+
jwt_validator.verify(token, { nonce: 'noncey' })
|
346
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
347
|
+
message: "Nonce (nonce) claim value mismatch in the ID token; expected (noncey), found (mismatch)"
|
348
|
+
}))
|
349
|
+
end
|
350
|
+
|
351
|
+
it 'should fail when “aud” is an array of strings and azp claim is not present' do
|
352
|
+
aud = [
|
353
|
+
client_id,
|
354
|
+
"https://#{domain}/userinfo"
|
355
|
+
]
|
356
|
+
payload = {
|
357
|
+
iss: "https://#{domain}/",
|
358
|
+
sub: 'sub',
|
359
|
+
aud: aud,
|
360
|
+
exp: future_timecode,
|
361
|
+
iat: past_timecode
|
362
|
+
}
|
363
|
+
|
364
|
+
token = make_hs256_token(payload)
|
365
|
+
expect do
|
366
|
+
jwt_validator.verify(token)
|
367
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
368
|
+
message: "Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values"
|
369
|
+
}))
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'should fail when "azp" claim doesnt match the expected aud' do
|
373
|
+
aud = [
|
374
|
+
client_id,
|
375
|
+
"https://#{domain}/userinfo"
|
376
|
+
]
|
377
|
+
payload = {
|
378
|
+
iss: "https://#{domain}/",
|
379
|
+
sub: 'sub',
|
380
|
+
aud: aud,
|
381
|
+
exp: future_timecode,
|
382
|
+
iat: past_timecode,
|
383
|
+
azp: 'not_expected'
|
384
|
+
}
|
385
|
+
|
386
|
+
token = make_hs256_token(payload)
|
387
|
+
expect do
|
388
|
+
jwt_validator.verify(token)
|
389
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
390
|
+
message: "Authorized Party (azp) claim mismatch in the ID token; expected (#{client_id}), found (not_expected)"
|
391
|
+
}))
|
392
|
+
end
|
393
|
+
|
394
|
+
it 'should fail when “max_age” sent on the authentication request and this claim is not present' do
|
395
|
+
payload = {
|
396
|
+
iss: "https://#{domain}/",
|
397
|
+
sub: 'sub',
|
398
|
+
aud: client_id,
|
399
|
+
exp: future_timecode,
|
400
|
+
iat: past_timecode
|
401
|
+
}
|
402
|
+
|
403
|
+
token = make_hs256_token(payload)
|
404
|
+
expect do
|
405
|
+
jwt_validator.verify(token, { max_age: 60 })
|
406
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
407
|
+
message: "Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified"
|
408
|
+
}))
|
409
|
+
end
|
410
|
+
|
411
|
+
it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future' do
|
412
|
+
payload = {
|
413
|
+
iss: "https://#{domain}/",
|
414
|
+
sub: 'sub',
|
415
|
+
aud: client_id,
|
416
|
+
exp: future_timecode,
|
417
|
+
iat: past_timecode,
|
418
|
+
auth_time: past_timecode
|
419
|
+
}
|
420
|
+
|
421
|
+
token = make_hs256_token(payload)
|
422
|
+
expect do
|
423
|
+
jwt_validator.verify(token, { max_age: 60 })
|
424
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
425
|
+
message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(past_timecode + 60 + 60)})"
|
426
|
+
}))
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
|
430
|
+
now = Time.now.to_i
|
431
|
+
auth_time = now - 121
|
432
|
+
max_age = 60
|
433
|
+
payload = {
|
434
|
+
iss: "https://#{domain}/",
|
435
|
+
sub: 'sub',
|
436
|
+
aud: client_id,
|
437
|
+
exp: future_timecode,
|
438
|
+
iat: past_timecode,
|
439
|
+
auth_time: auth_time
|
440
|
+
}
|
441
|
+
|
442
|
+
token = make_hs256_token(payload)
|
443
|
+
expect do
|
444
|
+
jwt_validator.verify(token, { max_age: max_age })
|
445
|
+
# Time.at(auth_time + max_age + leeway
|
446
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
447
|
+
message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + 60)})"
|
448
|
+
}))
|
449
|
+
end
|
450
|
+
|
451
|
+
it 'should verify when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
|
452
|
+
now = Time.now.to_i
|
453
|
+
auth_time = now - 119
|
454
|
+
max_age = 60
|
455
|
+
payload = {
|
456
|
+
iss: "https://#{domain}/",
|
457
|
+
sub: 'sub',
|
458
|
+
aud: client_id,
|
459
|
+
exp: future_timecode,
|
460
|
+
iat: past_timecode,
|
461
|
+
auth_time: auth_time
|
462
|
+
}
|
463
|
+
|
464
|
+
token = make_hs256_token(payload)
|
465
|
+
id_token = jwt_validator.verify(token, { max_age: max_age })
|
466
|
+
expect(id_token['auth_time']).to eq(auth_time)
|
467
|
+
end
|
468
|
+
|
469
|
+
it 'should fail for RS256 token when kid is incorrect' do
|
470
|
+
domain = 'example.org'
|
203
471
|
sub = 'abc123'
|
204
472
|
payload = {
|
205
473
|
sub: sub,
|
@@ -208,12 +476,16 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
208
476
|
iat: past_timecode,
|
209
477
|
aud: client_id
|
210
478
|
}
|
211
|
-
|
212
|
-
|
213
|
-
expect
|
479
|
+
invalid_kid = 'invalid-kid'
|
480
|
+
token = make_rs256_token(payload, invalid_kid)
|
481
|
+
expect do
|
482
|
+
verified_token = make_jwt_validator(opt_domain: domain).verify(token)
|
483
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
484
|
+
message: "Could not find a public key for Key ID (kid) 'invalid-kid'"
|
485
|
+
}))
|
214
486
|
end
|
215
487
|
|
216
|
-
it 'should
|
488
|
+
it 'should fail when RS256 token has invalid signature' do
|
217
489
|
domain = 'example.org'
|
218
490
|
sub = 'abc123'
|
219
491
|
payload = {
|
@@ -221,35 +493,150 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
221
493
|
exp: future_timecode,
|
222
494
|
iss: "https://#{domain}/",
|
223
495
|
iat: past_timecode,
|
496
|
+
aud: client_id
|
497
|
+
}
|
498
|
+
token = make_rs256_token(payload) + 'bad'
|
499
|
+
expect do
|
500
|
+
verified_token = make_jwt_validator(opt_domain: domain).verify(token)
|
501
|
+
end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({
|
502
|
+
message: "Signature verification raised"
|
503
|
+
}))
|
504
|
+
end
|
505
|
+
|
506
|
+
it 'should fail when algorithm is not RS256 or HS256' do
|
507
|
+
payload = {
|
508
|
+
iss: "https://#{domain}/",
|
509
|
+
sub: 'abc123',
|
510
|
+
aud: client_id,
|
511
|
+
exp: future_timecode,
|
512
|
+
iat: past_timecode
|
513
|
+
}
|
514
|
+
token = JWT.encode payload, 'secret', 'HS384'
|
515
|
+
expect do
|
516
|
+
jwt_validator.verify(token)
|
517
|
+
end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
|
518
|
+
message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256"
|
519
|
+
}))
|
520
|
+
end
|
521
|
+
|
522
|
+
it 'should fail when HS256 token has invalid signature' do
|
523
|
+
payload = {
|
524
|
+
iss: "https://#{domain}/",
|
525
|
+
sub: 'abc123',
|
526
|
+
aud: client_id,
|
527
|
+
exp: future_timecode,
|
528
|
+
iat: past_timecode
|
529
|
+
}
|
530
|
+
token = make_hs256_token(payload, 'bad_secret')
|
531
|
+
expect do
|
532
|
+
# validator is configured to use "CLIENT_SECRET" by default
|
533
|
+
jwt_validator.verify(token)
|
534
|
+
end.to raise_error(an_instance_of(JWT::VerificationError))
|
535
|
+
end
|
536
|
+
|
537
|
+
it 'should verify a valid HS256 token with multiple audiences' do
|
538
|
+
audience = [
|
539
|
+
client_id,
|
540
|
+
"https://#{domain}/userinfo"
|
541
|
+
]
|
542
|
+
payload = {
|
543
|
+
iss: "https://#{domain}/",
|
544
|
+
sub: 'sub',
|
545
|
+
aud: audience,
|
546
|
+
exp: future_timecode,
|
547
|
+
iat: past_timecode,
|
548
|
+
azp: client_id
|
549
|
+
}
|
550
|
+
token = make_hs256_token(payload)
|
551
|
+
id_token = jwt_validator.verify(token)
|
552
|
+
expect(id_token['aud']).to eq(audience)
|
553
|
+
end
|
554
|
+
|
555
|
+
it 'should verify a standard HS256 token' do
|
556
|
+
sub = 'abc123'
|
557
|
+
payload = {
|
558
|
+
iss: "https://#{domain}/",
|
559
|
+
sub: sub,
|
224
560
|
aud: client_id,
|
225
|
-
|
561
|
+
exp: future_timecode,
|
562
|
+
iat: past_timecode
|
563
|
+
}
|
564
|
+
token = make_hs256_token(payload)
|
565
|
+
verified_token = jwt_validator.verify(token)
|
566
|
+
expect(verified_token['sub']).to eq(sub)
|
567
|
+
end
|
568
|
+
|
569
|
+
it 'should verify a standard RS256 token' do
|
570
|
+
domain = 'example.org'
|
571
|
+
sub = 'abc123'
|
572
|
+
payload = {
|
573
|
+
sub: sub,
|
574
|
+
exp: future_timecode,
|
575
|
+
iss: "https://#{domain}/",
|
576
|
+
iat: past_timecode,
|
577
|
+
aud: client_id
|
226
578
|
}
|
227
579
|
token = make_rs256_token(payload)
|
228
|
-
|
229
|
-
expect(
|
580
|
+
verified_token = make_jwt_validator(opt_domain: domain).verify(token)
|
581
|
+
expect(verified_token['sub']).to eq(sub)
|
582
|
+
end
|
583
|
+
|
584
|
+
it 'should verify a HS256 JWT signature when calling verify signature directly' do
|
585
|
+
sub = 'abc123'
|
586
|
+
payload = {
|
587
|
+
iss: "https://#{domain}/",
|
588
|
+
sub: sub,
|
589
|
+
aud: client_id,
|
590
|
+
exp: future_timecode,
|
591
|
+
iat: past_timecode
|
592
|
+
}
|
593
|
+
token = make_hs256_token(payload)
|
594
|
+
verified_token_signature = jwt_validator.verify_signature(token)
|
595
|
+
expect(verified_token_signature[0]).to eq('CLIENT_SECRET')
|
596
|
+
expect(verified_token_signature[1]).to eq('HS256')
|
597
|
+
end
|
598
|
+
|
599
|
+
it 'should verify a RS256 JWT signature verify signature directly' do
|
600
|
+
domain = 'example.org'
|
601
|
+
sub = 'abc123'
|
602
|
+
payload = {
|
603
|
+
sub: sub,
|
604
|
+
exp: future_timecode,
|
605
|
+
iss: "https://#{domain}/",
|
606
|
+
iat: past_timecode,
|
607
|
+
aud: client_id
|
608
|
+
}
|
609
|
+
token = make_rs256_token(payload)
|
610
|
+
verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token)
|
611
|
+
expect(verified_token_signature.length).to be(2)
|
612
|
+
expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA)
|
613
|
+
expect(verified_token_signature[1]).to eq('RS256')
|
230
614
|
end
|
231
615
|
end
|
232
616
|
|
233
617
|
private
|
234
618
|
|
235
|
-
def make_jwt_validator(opt_domain
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
client_secret
|
241
|
-
)
|
619
|
+
def make_jwt_validator(opt_domain: domain, opt_issuer: nil)
|
620
|
+
opts = OpenStruct.new(
|
621
|
+
domain: opt_domain,
|
622
|
+
client_id: client_id,
|
623
|
+
client_secret: client_secret
|
242
624
|
)
|
625
|
+
opts[:issuer] = opt_issuer unless opt_issuer.nil?
|
626
|
+
|
627
|
+
OmniAuth::Auth0::JWTValidator.new(opts)
|
243
628
|
end
|
244
629
|
|
245
|
-
def make_hs256_token(payload = nil)
|
630
|
+
def make_hs256_token(payload = nil, secret = nil)
|
246
631
|
payload = { sub: 'abc123' } if payload.nil?
|
247
|
-
|
632
|
+
secret = client_secret if secret.nil?
|
633
|
+
JWT.encode payload, secret, 'HS256'
|
248
634
|
end
|
249
635
|
|
250
|
-
def make_rs256_token(payload = nil)
|
636
|
+
def make_rs256_token(payload = nil, kid = nil)
|
251
637
|
payload = { sub: 'abc123' } if payload.nil?
|
252
|
-
|
638
|
+
kid = valid_jwks_kid if kid.nil?
|
639
|
+
JWT.encode payload, rsa_private_key, 'RS256', kid: kid
|
253
640
|
end
|
254
641
|
|
255
642
|
def make_cert(private_key)
|
@@ -277,7 +664,7 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
277
664
|
cert.sign private_key, OpenSSL::Digest::SHA1.new
|
278
665
|
end
|
279
666
|
|
280
|
-
def
|
667
|
+
def stub_complete_jwks
|
281
668
|
stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json')
|
282
669
|
.to_return(
|
283
670
|
headers: { 'Content-Type' => 'application/json' },
|
@@ -286,18 +673,11 @@ describe OmniAuth::Auth0::JWTValidator do
|
|
286
673
|
)
|
287
674
|
end
|
288
675
|
|
289
|
-
def
|
290
|
-
stub_request(:get, 'https://samples.auth0.com/.well-known/jwks-bad.json')
|
291
|
-
.to_return(
|
292
|
-
status: 404
|
293
|
-
)
|
294
|
-
end
|
295
|
-
|
296
|
-
def stub_dummy_jwks
|
676
|
+
def stub_expected_jwks
|
297
677
|
stub_request(:get, 'https://example.org/.well-known/jwks.json')
|
298
678
|
.to_return(
|
299
679
|
headers: { 'Content-Type' => 'application/json' },
|
300
|
-
body:
|
680
|
+
body: valid_jwks,
|
301
681
|
status: 200
|
302
682
|
)
|
303
683
|
end
|