omniauth-auth0 2.0.0 → 2.4.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.

Potentially problematic release.


This version of omniauth-auth0 might be problematic. Click here for more details.

@@ -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,19 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'uri'
5
+ require 'securerandom'
3
6
  require 'omniauth-oauth2'
7
+ require 'omniauth/auth0/jwt_validator'
8
+ require 'omniauth/auth0/telemetry'
9
+ require 'omniauth/auth0/errors'
4
10
 
5
11
  module OmniAuth
6
12
  module Strategies
7
13
  # Auth0 OmniAuth strategy
8
14
  class Auth0 < OmniAuth::Strategies::OAuth2
15
+ include OmniAuth::Auth0::Telemetry
16
+
9
17
  option :name, 'auth0'
10
18
 
11
- args [
12
- :client_id,
13
- :client_secret,
14
- :domain
19
+ args %i[
20
+ client_id
21
+ client_secret
22
+ domain
15
23
  ]
16
24
 
25
+ # Setup client URLs used during authentication
17
26
  def client
18
27
  options.client_options.site = domain_url
19
28
  options.client_options.authorize_url = '/authorize'
@@ -22,25 +31,48 @@ module OmniAuth
22
31
  super
23
32
  end
24
33
 
34
+ # Use the "sub" key of the userinfo returned
35
+ # as the uid (globally unique string identifier).
25
36
  uid { raw_info['sub'] }
26
37
 
38
+ # Build the API credentials hash with returned auth data.
27
39
  credentials do
28
- hash = { 'token' => access_token.token }
29
- hash['expires'] = true
40
+ credentials = {
41
+ 'token' => access_token.token,
42
+ 'expires' => true
43
+ }
44
+
30
45
  if access_token.params
31
- hash['id_token'] = access_token.params['id_token']
32
- hash['token_type'] = access_token.params['token_type']
33
- hash['refresh_token'] = access_token.refresh_token
46
+ credentials.merge!(
47
+ 'id_token' => access_token.params['id_token'],
48
+ 'token_type' => access_token.params['token_type'],
49
+ 'refresh_token' => access_token.refresh_token
50
+ )
51
+ end
52
+
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)
34
62
  end
35
- hash
63
+
64
+ credentials
36
65
  end
37
66
 
67
+ # Store all raw information for use in the session.
38
68
  extra do
39
69
  {
40
70
  raw_info: raw_info
41
71
  }
42
72
  end
43
73
 
74
+ # Build a hash of information about the user
75
+ # with keys taken from the Auth Hash Schema.
44
76
  info do
45
77
  {
46
78
  name: raw_info['name'] || raw_info['sub'],
@@ -50,56 +82,82 @@ module OmniAuth
50
82
  }
51
83
  end
52
84
 
85
+ # Define the parameters used for the /authorize endpoint
53
86
  def authorize_params
54
87
  params = super
55
- params['auth0Client'] = client_info
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
+
56
101
  params
57
102
  end
58
103
 
104
+ def build_access_token
105
+ options.token_params[:headers] = { 'Auth0-Client' => telemetry_encoded }
106
+ super
107
+ end
108
+
109
+ # Declarative override for the request phase of authentication
59
110
  def request_phase
60
111
  if no_client_id?
112
+ # Do we have a client_id for this Application?
61
113
  fail!(:missing_client_id)
62
114
  elsif no_client_secret?
115
+ # Do we have a client_secret for this Application?
63
116
  fail!(:missing_client_secret)
64
117
  elsif no_domain?
118
+ # Do we have a domain for this Application?
65
119
  fail!(:missing_domain)
66
120
  else
121
+ # All checks pass, run the Oauth2 request_phase method.
67
122
  super
68
123
  end
69
124
  end
70
125
 
126
+ def callback_phase
127
+ super
128
+ rescue OmniAuth::Auth0::TokenValidationError => e
129
+ fail!(:token_validation_error, e)
130
+ end
131
+
71
132
  private
72
133
 
134
+ # Parse the raw user info.
73
135
  def raw_info
74
136
  userinfo_url = options.client_options.userinfo_url
75
137
  @raw_info ||= access_token.get(userinfo_url).parsed
76
138
  end
77
139
 
140
+ # Check if the options include a client_id
78
141
  def no_client_id?
79
142
  ['', nil].include?(options.client_id)
80
143
  end
81
144
 
145
+ # Check if the options include a client_secret
82
146
  def no_client_secret?
83
147
  ['', nil].include?(options.client_secret)
84
148
  end
85
149
 
150
+ # Check if the options include a domain
86
151
  def no_domain?
87
152
  ['', nil].include?(options.domain)
88
153
  end
89
154
 
155
+ # Normalize a domain to a URL.
90
156
  def domain_url
91
157
  domain_url = URI(options.domain)
92
158
  domain_url = URI("https://#{domain_url}") if domain_url.scheme.nil?
93
159
  domain_url.to_s
94
160
  end
95
-
96
- def client_info
97
- client_info = JSON.dump(
98
- name: 'omniauth-auth0',
99
- version: OmniAuth::Auth0::VERSION
100
- )
101
- Base64.urlsafe_encode64(client_info)
102
- end
103
161
  end
104
162
  end
105
163
  end
@@ -8,22 +8,20 @@ Gem::Specification.new do |s|
8
8
  s.authors = ['Auth0']
9
9
  s.email = ['info@auth0.com']
10
10
  s.homepage = 'https://github.com/auth0/omniauth-auth0'
11
- s.summary = 'Omniauth OAuth2 strategy for the Auth0 platform.'
11
+ s.summary = 'OmniAuth OAuth2 strategy for the Auth0 platform.'
12
12
  s.description = %q{Auth0 is an authentication broker that supports social identity providers as well as enterprise identity providers such as Active Directory, LDAP, Google Apps, Salesforce.
13
13
 
14
14
  OmniAuth is a library that standardizes multi-provider authentication for web applications. It was created to be powerful, flexible, and do as little as possible.
15
15
 
16
- omniauth-auth0 is the omniauth strategy for Auth0.
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) }
24
22
  s.require_paths = ['lib']
25
23
 
26
- s.add_runtime_dependency 'omniauth-oauth2', '~> 1.4'
24
+ s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5'
27
25
 
28
26
  s.add_development_dependency 'bundler', '~> 1.9'
29
27
 
@@ -0,0 +1,501 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+ require 'jwt'
4
+
5
+ describe OmniAuth::Auth0::JWTValidator do
6
+ #
7
+ # Reused data
8
+ #
9
+
10
+ let(:client_id) { 'CLIENT_ID' }
11
+ let(:client_secret) { 'CLIENT_SECRET' }
12
+ let(:domain) { 'samples.auth0.com' }
13
+ let(:future_timecode) { 32_503_680_000 }
14
+ let(:past_timecode) { 303_912_000 }
15
+ let(:jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
16
+
17
+ let(:rsa_private_key) do
18
+ OpenSSL::PKey::RSA.generate 2048
19
+ end
20
+
21
+ let(:rsa_token_jwks) do
22
+ {
23
+ keys: [
24
+ {
25
+ kid: jwks_kid,
26
+ x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)]
27
+ }
28
+ ]
29
+ }.to_json
30
+ end
31
+
32
+ let(:jwks) do
33
+ current_dir = File.dirname(__FILE__)
34
+ jwks_file = File.read("#{current_dir}/../../resources/jwks.json")
35
+ JSON.parse(jwks_file, symbolize_names: true)
36
+ end
37
+
38
+ #
39
+ # Specs
40
+ #
41
+
42
+ describe 'JWT verifier default values' do
43
+ let(:jwt_validator) do
44
+ make_jwt_validator
45
+ end
46
+
47
+ it 'should have the correct issuer' do
48
+ expect(jwt_validator.issuer).to eq('https://samples.auth0.com/')
49
+ end
50
+ end
51
+
52
+ describe 'JWT verifier token_head' do
53
+ let(:jwt_validator) do
54
+ make_jwt_validator
55
+ end
56
+
57
+ it 'should parse the head of a valid JWT' do
58
+ expect(jwt_validator.token_head(make_hs256_token)[:alg]).to eq('HS256')
59
+ end
60
+
61
+ it 'should fail parsing the head of a blank JWT' do
62
+ expect(jwt_validator.token_head('')).to eq({})
63
+ end
64
+
65
+ it 'should fail parsing the head of an invalid JWT' do
66
+ expect(jwt_validator.token_head('.')).to eq({})
67
+ end
68
+
69
+ it 'should throw an exception for invalid JSON' do
70
+ expect do
71
+ jwt_validator.token_head('QXV0aDA=')
72
+ end.to raise_error(JSON::ParserError)
73
+ end
74
+ end
75
+
76
+ describe 'JWT verifier jwks_public_cert' do
77
+ let(:jwt_validator) do
78
+ make_jwt_validator
79
+ end
80
+
81
+ it 'should return a public_key' do
82
+ x5c = jwks[:keys].first[:x5c].first
83
+ public_cert = jwt_validator.jwks_public_cert(x5c)
84
+ expect(public_cert.instance_of?(OpenSSL::PKey::RSA)).to eq(true)
85
+ end
86
+
87
+ it 'should fail with an invalid x5c' do
88
+ expect do
89
+ jwt_validator.jwks_public_cert('QXV0aDA=')
90
+ end.to raise_error(OpenSSL::X509::CertificateError)
91
+ end
92
+ end
93
+
94
+ describe 'JWT verifier jwks_key' do
95
+ let(:jwt_validator) do
96
+ make_jwt_validator
97
+ end
98
+
99
+ before do
100
+ stub_jwks
101
+ end
102
+
103
+ it 'should return a key' do
104
+ expect(jwt_validator.jwks_key(:alg, jwks_kid)).to eq('RS256')
105
+ end
106
+
107
+ it 'should return an x5c key' do
108
+ expect(jwt_validator.jwks_key(:x5c, jwks_kid).length).to eq(1)
109
+ end
110
+
111
+ it 'should return nil if there is not key' do
112
+ expect(jwt_validator.jwks_key(:auth0, jwks_kid)).to eq(nil)
113
+ end
114
+
115
+ it 'should return nil if the key ID is invalid' do
116
+ expect(jwt_validator.jwks_key(:alg, "#{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
147
+ end
148
+ end
149
+
150
+ describe 'JWT verifier verify' do
151
+ let(:jwt_validator) do
152
+ make_jwt_validator
153
+ end
154
+
155
+ before do
156
+ stub_jwks
157
+ stub_dummy_jwks
158
+ end
159
+
160
+ it 'should fail with missing issuer' do
161
+ expect do
162
+ jwt_validator.verify(make_hs256_token)
163
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
164
+ message: "Issuer (iss) claim must be a string present in the ID token"
165
+ }))
166
+ end
167
+
168
+ it 'should fail with invalid issuer' do
169
+ payload = {
170
+ iss: 'https://auth0.com/'
171
+ }
172
+ token = make_hs256_token(payload)
173
+ expect do
174
+ jwt_validator.verify(token)
175
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
176
+ message: "Issuer (iss) claim mismatch in the ID token, expected (https://samples.auth0.com/), found (https://auth0.com/)"
177
+ }))
178
+ end
179
+
180
+ it 'should fail when subject is missing' do
181
+ payload = {
182
+ iss: "https://#{domain}/",
183
+ sub: ''
184
+ }
185
+ token = make_hs256_token(payload)
186
+ expect do
187
+ jwt_validator.verify(token)
188
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
189
+ message: "Subject (sub) claim must be a string present in the ID token"
190
+ }))
191
+ end
192
+
193
+ it 'should fail with missing audience' do
194
+ payload = {
195
+ iss: "https://#{domain}/",
196
+ sub: 'sub'
197
+ }
198
+ token = make_hs256_token(payload)
199
+ expect do
200
+ jwt_validator.verify(token)
201
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
202
+ message: "Audience (aud) claim must be a string or array of strings present in the ID token"
203
+ }))
204
+ end
205
+
206
+ it 'should fail with invalid audience' do
207
+ payload = {
208
+ iss: "https://#{domain}/",
209
+ sub: 'sub',
210
+ aud: 'Auth0'
211
+ }
212
+ token = make_hs256_token(payload)
213
+ expect do
214
+ jwt_validator.verify(token)
215
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
216
+ message: "Audience (aud) claim mismatch in the ID token; expected #{client_id} but found Auth0"
217
+ }))
218
+ end
219
+
220
+ it 'should fail when missing expiration' do
221
+ payload = {
222
+ iss: "https://#{domain}/",
223
+ sub: 'sub',
224
+ aud: client_id
225
+ }
226
+
227
+ token = make_hs256_token(payload)
228
+ expect do
229
+ jwt_validator.verify(token)
230
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
231
+ message: "Expiration time (exp) claim must be a number present in the ID token"
232
+ }))
233
+ end
234
+
235
+ it 'should fail when past expiration' do
236
+ payload = {
237
+ iss: "https://#{domain}/",
238
+ sub: 'sub',
239
+ aud: client_id,
240
+ exp: past_timecode
241
+ }
242
+
243
+ token = make_hs256_token(payload)
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 error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(past_timecode + 60)})"
248
+ }))
249
+ end
250
+
251
+ it 'should fail when missing iat' do
252
+ payload = {
253
+ iss: "https://#{domain}/",
254
+ sub: 'sub',
255
+ aud: client_id,
256
+ exp: future_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: "Issued At (iat) claim must be a number present in the ID token"
264
+ }))
265
+ end
266
+
267
+ it 'should fail when authorize params has nonce but nonce is missing in the token' do
268
+ payload = {
269
+ iss: "https://#{domain}/",
270
+ sub: 'sub',
271
+ aud: client_id,
272
+ exp: future_timecode,
273
+ iat: past_timecode
274
+ }
275
+
276
+ token = make_hs256_token(payload)
277
+ expect do
278
+ jwt_validator.verify(token, { nonce: 'noncey' })
279
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
280
+ message: "Nonce (nonce) claim must be a string present in the ID token"
281
+ }))
282
+ end
283
+
284
+ it 'should fail when authorize params has nonce but token nonce does not match' do
285
+ payload = {
286
+ iss: "https://#{domain}/",
287
+ sub: 'sub',
288
+ aud: client_id,
289
+ exp: future_timecode,
290
+ iat: past_timecode,
291
+ nonce: 'mismatch'
292
+ }
293
+
294
+ token = make_hs256_token(payload)
295
+ expect do
296
+ jwt_validator.verify(token, { nonce: 'noncey' })
297
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
298
+ message: "Nonce (nonce) claim value mismatch in the ID token; expected (noncey), found (mismatch)"
299
+ }))
300
+ end
301
+
302
+ it 'should fail when “aud” is an array of strings and azp claim is not present' do
303
+ aud = [
304
+ client_id,
305
+ "https://#{domain}/userinfo"
306
+ ]
307
+ payload = {
308
+ iss: "https://#{domain}/",
309
+ sub: 'sub',
310
+ aud: aud,
311
+ exp: future_timecode,
312
+ iat: past_timecode
313
+ }
314
+
315
+ token = make_hs256_token(payload)
316
+ expect do
317
+ jwt_validator.verify(token)
318
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
319
+ message: "Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values"
320
+ }))
321
+ end
322
+
323
+ it 'should fail when "azp" claim doesnt match the expected aud' do
324
+ aud = [
325
+ client_id,
326
+ "https://#{domain}/userinfo"
327
+ ]
328
+ payload = {
329
+ iss: "https://#{domain}/",
330
+ sub: 'sub',
331
+ aud: aud,
332
+ exp: future_timecode,
333
+ iat: past_timecode,
334
+ azp: 'not_expected'
335
+ }
336
+
337
+ token = make_hs256_token(payload)
338
+ expect do
339
+ jwt_validator.verify(token)
340
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
341
+ message: "Authorized Party (azp) claim mismatch in the ID token; expected (#{client_id}), found (not_expected)"
342
+ }))
343
+ end
344
+
345
+ it 'should fail when “max_age” sent on the authentication request and this claim is not present' do
346
+ payload = {
347
+ iss: "https://#{domain}/",
348
+ sub: 'sub',
349
+ aud: client_id,
350
+ exp: future_timecode,
351
+ iat: past_timecode
352
+ }
353
+
354
+ token = make_hs256_token(payload)
355
+ expect do
356
+ jwt_validator.verify(token, { max_age: 60 })
357
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
358
+ message: "Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified"
359
+ }))
360
+ end
361
+
362
+ 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
363
+ payload = {
364
+ iss: "https://#{domain}/",
365
+ sub: 'sub',
366
+ aud: client_id,
367
+ exp: future_timecode,
368
+ iat: past_timecode,
369
+ auth_time: past_timecode
370
+ }
371
+
372
+ token = make_hs256_token(payload)
373
+ expect do
374
+ jwt_validator.verify(token, { max_age: 60 })
375
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
376
+ 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)})"
377
+ }))
378
+ end
379
+
380
+ it 'should verify a valid HS256 token with multiple audiences' do
381
+ audience = [
382
+ client_id,
383
+ "https://#{domain}/userinfo"
384
+ ]
385
+ payload = {
386
+ iss: "https://#{domain}/",
387
+ sub: 'sub',
388
+ aud: audience,
389
+ exp: future_timecode,
390
+ iat: past_timecode,
391
+ azp: client_id
392
+ }
393
+ token = make_hs256_token(payload)
394
+ id_token = jwt_validator.verify(token)
395
+ expect(id_token['aud']).to eq(audience)
396
+ end
397
+
398
+ it 'should verify a standard HS256 token' do
399
+ sub = 'abc123'
400
+ payload = {
401
+ iss: "https://#{domain}/",
402
+ sub: sub,
403
+ aud: client_id,
404
+ exp: future_timecode,
405
+ iat: past_timecode
406
+ }
407
+ token = make_hs256_token(payload)
408
+ verified_token = jwt_validator.verify(token)
409
+ expect(verified_token['sub']).to eq(sub)
410
+ end
411
+
412
+ it 'should verify a standard RS256 token' do
413
+ domain = 'example.org'
414
+ sub = 'abc123'
415
+ payload = {
416
+ sub: sub,
417
+ exp: future_timecode,
418
+ iss: "https://#{domain}/",
419
+ iat: past_timecode,
420
+ aud: client_id,
421
+ kid: jwks_kid
422
+ }
423
+ token = make_rs256_token(payload)
424
+ verified_token = make_jwt_validator(opt_domain: domain).verify(token)
425
+ expect(verified_token['sub']).to eq(sub)
426
+ end
427
+ end
428
+
429
+ private
430
+
431
+ def make_jwt_validator(opt_domain: domain, opt_issuer: nil)
432
+ opts = OpenStruct.new(
433
+ domain: opt_domain,
434
+ client_id: client_id,
435
+ client_secret: client_secret
436
+ )
437
+ opts[:issuer] = opt_issuer unless opt_issuer.nil?
438
+
439
+ OmniAuth::Auth0::JWTValidator.new(opts)
440
+ end
441
+
442
+ def make_hs256_token(payload = nil)
443
+ payload = { sub: 'abc123' } if payload.nil?
444
+ JWT.encode payload, client_secret, 'HS256'
445
+ end
446
+
447
+ def make_rs256_token(payload = nil)
448
+ payload = { sub: 'abc123' } if payload.nil?
449
+ JWT.encode payload, rsa_private_key, 'RS256', kid: jwks_kid
450
+ end
451
+
452
+ def make_cert(private_key)
453
+ cert = OpenSSL::X509::Certificate.new
454
+ cert.issuer = OpenSSL::X509::Name.parse('/C=BE/O=Auth0/OU=Auth0/CN=Auth0')
455
+ cert.subject = cert.issuer
456
+ cert.not_before = Time.now
457
+ cert.not_after = Time.now + 365 * 24 * 60 * 60
458
+ cert.public_key = private_key.public_key
459
+ cert.serial = 0x0
460
+ cert.version = 2
461
+
462
+ ef = OpenSSL::X509::ExtensionFactory.new
463
+ ef.subject_certificate = cert
464
+ ef.issuer_certificate = cert
465
+ cert.extensions = [
466
+ ef.create_extension('basicConstraints', 'CA:TRUE', true),
467
+ ef.create_extension('subjectKeyIdentifier', 'hash')
468
+ ]
469
+ cert.add_extension ef.create_extension(
470
+ 'authorityKeyIdentifier',
471
+ 'keyid:always,issuer:always'
472
+ )
473
+
474
+ cert.sign private_key, OpenSSL::Digest::SHA1.new
475
+ end
476
+
477
+ def stub_jwks
478
+ stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json')
479
+ .to_return(
480
+ headers: { 'Content-Type' => 'application/json' },
481
+ body: jwks.to_json,
482
+ status: 200
483
+ )
484
+ end
485
+
486
+ def stub_bad_jwks
487
+ stub_request(:get, 'https://samples.auth0.com/.well-known/jwks-bad.json')
488
+ .to_return(
489
+ status: 404
490
+ )
491
+ end
492
+
493
+ def stub_dummy_jwks
494
+ stub_request(:get, 'https://example.org/.well-known/jwks.json')
495
+ .to_return(
496
+ headers: { 'Content-Type' => 'application/json' },
497
+ body: rsa_token_jwks,
498
+ status: 200
499
+ )
500
+ end
501
+ end