omniauth-auth0 2.3.0 → 2.5.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.
@@ -6,6 +6,7 @@ require 'securerandom'
6
6
  require 'omniauth-oauth2'
7
7
  require 'omniauth/auth0/jwt_validator'
8
8
  require 'omniauth/auth0/telemetry'
9
+ require 'omniauth/auth0/errors'
9
10
 
10
11
  module OmniAuth
11
12
  module Strategies
@@ -56,8 +57,7 @@ module OmniAuth
56
57
  auth_scope = session_authorize_params[:scope]
57
58
  if auth_scope.respond_to?(:include?) && auth_scope.include?('openid')
58
59
  # Make sure the ID token can be verified and decoded.
59
- auth0_jwt = OmniAuth::Auth0::JWTValidator.new(options)
60
- auth0_jwt.verify(credentials['id_token'], session_authorize_params)
60
+ jwt_validator.verify(credentials['id_token'], session_authorize_params)
61
61
  end
62
62
 
63
63
  credentials
@@ -84,16 +84,15 @@ module OmniAuth
84
84
  # Define the parameters used for the /authorize endpoint
85
85
  def authorize_params
86
86
  params = super
87
- parsed_query = Rack::Utils.parse_query(request.query_string)
88
- %w[connection prompt].each do |key|
89
- params[key] = parsed_query[key] if parsed_query.key?(key)
87
+ %w[connection connection_scope prompt screen_hint].each do |key|
88
+ params[key] = request.params[key] if request.params.key?(key)
90
89
  end
91
90
 
92
91
  # Generate nonce
93
92
  params[:nonce] = SecureRandom.hex
94
93
  # Generate leeway if none exists
95
94
  params[:leeway] = 60 unless params[:leeway]
96
-
95
+
97
96
  # Store authorize params in the session for token verification
98
97
  session['authorize_params'] = params
99
98
 
@@ -129,11 +128,23 @@ module OmniAuth
129
128
  end
130
129
 
131
130
  private
131
+ def jwt_validator
132
+ @jwt_validator ||= OmniAuth::Auth0::JWTValidator.new(options)
133
+ end
132
134
 
133
135
  # Parse the raw user info.
134
136
  def raw_info
135
- userinfo_url = options.client_options.userinfo_url
136
- @raw_info ||= access_token.get(userinfo_url).parsed
137
+ return @raw_info if @raw_info
138
+
139
+ if access_token["id_token"]
140
+ claims, header = jwt_validator.decode(access_token["id_token"])
141
+ @raw_info = claims
142
+ else
143
+ userinfo_url = options.client_options.userinfo_url
144
+ @raw_info = access_token.get(userinfo_url).parsed
145
+ end
146
+
147
+ return @raw_info
137
148
  end
138
149
 
139
150
  # Check if the options include a client_id
@@ -16,16 +16,15 @@ 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) }
24
22
  s.require_paths = ['lib']
25
23
 
24
+ s.add_runtime_dependency 'omniauth', '~> 1.9'
26
25
  s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5'
27
26
 
28
- s.add_development_dependency 'bundler', '~> 1.9'
27
+ s.add_development_dependency 'bundler'
29
28
 
30
29
  s.license = 'MIT'
31
30
  end
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'json'
3
3
  require 'jwt'
4
- require 'omniauth/auth0/errors'
5
4
 
6
5
  describe OmniAuth::Auth0::JWTValidator do
7
6
  #
@@ -13,17 +12,17 @@ describe OmniAuth::Auth0::JWTValidator do
13
12
  let(:domain) { 'samples.auth0.com' }
14
13
  let(:future_timecode) { 32_503_680_000 }
15
14
  let(:past_timecode) { 303_912_000 }
16
- let(:jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
15
+ let(:valid_jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
17
16
 
18
17
  let(:rsa_private_key) do
19
18
  OpenSSL::PKey::RSA.generate 2048
20
19
  end
21
20
 
22
- let(:rsa_token_jwks) do
21
+ let(:valid_jwks) do
23
22
  {
24
23
  keys: [
25
24
  {
26
- kid: jwks_kid,
25
+ kid: valid_jwks_kid,
27
26
  x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)]
28
27
  }
29
28
  ]
@@ -92,29 +91,29 @@ describe OmniAuth::Auth0::JWTValidator do
92
91
  end
93
92
  end
94
93
 
95
- describe 'JWT verifier jwks_key' do
94
+ describe 'JWT verifier jwks key parsing' do
96
95
  let(:jwt_validator) do
97
96
  make_jwt_validator
98
97
  end
99
98
 
100
99
  before do
101
- stub_jwks
100
+ stub_complete_jwks
102
101
  end
103
102
 
104
103
  it 'should return a key' do
105
- expect(jwt_validator.jwks_key(:alg, jwks_kid)).to eq('RS256')
104
+ expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256')
106
105
  end
107
106
 
108
107
  it 'should return an x5c key' do
109
- expect(jwt_validator.jwks_key(:x5c, jwks_kid).length).to eq(1)
108
+ expect(jwt_validator.jwks_key(:x5c, valid_jwks_kid).length).to eq(1)
110
109
  end
111
110
 
112
111
  it 'should return nil if there is not key' do
113
- expect(jwt_validator.jwks_key(:auth0, jwks_kid)).to eq(nil)
112
+ expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil)
114
113
  end
115
114
 
116
115
  it 'should return nil if the key ID is invalid' do
117
- expect(jwt_validator.jwks_key(:alg, "#{jwks_kid}_invalid")).to eq(nil)
116
+ expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil)
118
117
  end
119
118
  end
120
119
 
@@ -134,16 +133,26 @@ describe OmniAuth::Auth0::JWTValidator do
134
133
  end
135
134
 
136
135
  context 'different from domain' do
137
- let(:jwt_validator) do
138
- make_jwt_validator(opt_issuer: 'different.auth0.com')
136
+ shared_examples_for 'has correct issuer and domain' do
137
+ let(:jwt_validator) { make_jwt_validator(opt_issuer: opt_issuer) }
138
+
139
+ it 'should have the correct issuer' do
140
+ expect(jwt_validator.issuer).to eq('https://different.auth0.com/')
141
+ end
142
+
143
+ it 'should have the correct domain' do
144
+ expect(jwt_validator.domain).to eq('https://samples.auth0.com/')
145
+ end
139
146
  end
140
147
 
141
- it 'should have the correct issuer' do
142
- expect(jwt_validator.issuer).to eq('https://different.auth0.com/')
148
+ context 'without protocol and trailing slash' do
149
+ let(:opt_issuer) { 'different.auth0.com' }
150
+ it_behaves_like 'has correct issuer and domain'
143
151
  end
144
152
 
145
- it 'should have the correct domain' do
146
- expect(jwt_validator.domain).to eq('https://samples.auth0.com/')
153
+ context 'with protocol and trailing slash' do
154
+ let(:opt_issuer) { 'https://different.auth0.com/' }
155
+ it_behaves_like 'has correct issuer and domain'
147
156
  end
148
157
  end
149
158
  end
@@ -154,8 +163,24 @@ describe OmniAuth::Auth0::JWTValidator do
154
163
  end
155
164
 
156
165
  before do
157
- stub_jwks
158
- stub_dummy_jwks
166
+ stub_complete_jwks
167
+ stub_expected_jwks
168
+ end
169
+
170
+ it 'should fail when JWT is nil' do
171
+ expect do
172
+ jwt_validator.verify(nil)
173
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
174
+ message: "ID token is required but missing"
175
+ }))
176
+ end
177
+
178
+ it 'should fail when JWT is not well-formed' do
179
+ expect do
180
+ jwt_validator.verify('abc.123')
181
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
182
+ message: "ID token could not be decoded"
183
+ }))
159
184
  end
160
185
 
161
186
  it 'should fail with missing issuer' do
@@ -249,6 +274,39 @@ describe OmniAuth::Auth0::JWTValidator do
249
274
  }))
250
275
  end
251
276
 
277
+ it 'should pass when past expiration but within default leeway' do
278
+ exp = Time.now.to_i - 59
279
+ payload = {
280
+ iss: "https://#{domain}/",
281
+ sub: 'sub',
282
+ aud: client_id,
283
+ exp: exp,
284
+ iat: past_timecode
285
+ }
286
+
287
+ token = make_hs256_token(payload)
288
+ id_token = jwt_validator.verify(token)
289
+ expect(id_token['exp']).to eq(exp)
290
+ end
291
+
292
+ it 'should fail when past expiration and outside default leeway' do
293
+ exp = Time.now.to_i - 61
294
+ payload = {
295
+ iss: "https://#{domain}/",
296
+ sub: 'sub',
297
+ aud: client_id,
298
+ exp: exp,
299
+ iat: past_timecode
300
+ }
301
+
302
+ token = make_hs256_token(payload)
303
+ expect do
304
+ jwt_validator.verify(token)
305
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
306
+ message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(exp + 60)})"
307
+ }))
308
+ end
309
+
252
310
  it 'should fail when missing iat' do
253
311
  payload = {
254
312
  iss: "https://#{domain}/",
@@ -378,6 +436,114 @@ describe OmniAuth::Auth0::JWTValidator do
378
436
  }))
379
437
  end
380
438
 
439
+ 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
440
+ now = Time.now.to_i
441
+ auth_time = now - 121
442
+ max_age = 60
443
+ payload = {
444
+ iss: "https://#{domain}/",
445
+ sub: 'sub',
446
+ aud: client_id,
447
+ exp: future_timecode,
448
+ iat: past_timecode,
449
+ auth_time: auth_time
450
+ }
451
+
452
+ token = make_hs256_token(payload)
453
+ expect do
454
+ jwt_validator.verify(token, { max_age: max_age })
455
+ # Time.at(auth_time + max_age + leeway
456
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
457
+ 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)})"
458
+ }))
459
+ end
460
+
461
+ 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
462
+ now = Time.now.to_i
463
+ auth_time = now - 119
464
+ max_age = 60
465
+ payload = {
466
+ iss: "https://#{domain}/",
467
+ sub: 'sub',
468
+ aud: client_id,
469
+ exp: future_timecode,
470
+ iat: past_timecode,
471
+ auth_time: auth_time
472
+ }
473
+
474
+ token = make_hs256_token(payload)
475
+ id_token = jwt_validator.verify(token, { max_age: max_age })
476
+ expect(id_token['auth_time']).to eq(auth_time)
477
+ end
478
+
479
+ it 'should fail for RS256 token when kid is incorrect' do
480
+ domain = 'example.org'
481
+ sub = 'abc123'
482
+ payload = {
483
+ sub: sub,
484
+ exp: future_timecode,
485
+ iss: "https://#{domain}/",
486
+ iat: past_timecode,
487
+ aud: client_id
488
+ }
489
+ invalid_kid = 'invalid-kid'
490
+ token = make_rs256_token(payload, invalid_kid)
491
+ expect do
492
+ verified_token = make_jwt_validator(opt_domain: domain).verify(token)
493
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
494
+ message: "Could not find a public key for Key ID (kid) 'invalid-kid'"
495
+ }))
496
+ end
497
+
498
+ it 'should fail when RS256 token has invalid signature' do
499
+ domain = 'example.org'
500
+ sub = 'abc123'
501
+ payload = {
502
+ sub: sub,
503
+ exp: future_timecode,
504
+ iss: "https://#{domain}/",
505
+ iat: past_timecode,
506
+ aud: client_id
507
+ }
508
+ token = make_rs256_token(payload) + 'bad'
509
+ expect do
510
+ verified_token = make_jwt_validator(opt_domain: domain).verify(token)
511
+ end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({
512
+ message: "Signature verification raised"
513
+ }))
514
+ end
515
+
516
+ it 'should fail when algorithm is not RS256 or HS256' do
517
+ payload = {
518
+ iss: "https://#{domain}/",
519
+ sub: 'abc123',
520
+ aud: client_id,
521
+ exp: future_timecode,
522
+ iat: past_timecode
523
+ }
524
+ token = JWT.encode payload, 'secret', 'HS384'
525
+ expect do
526
+ jwt_validator.verify(token)
527
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
528
+ message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256"
529
+ }))
530
+ end
531
+
532
+ it 'should fail when HS256 token has invalid signature' do
533
+ payload = {
534
+ iss: "https://#{domain}/",
535
+ sub: 'abc123',
536
+ aud: client_id,
537
+ exp: future_timecode,
538
+ iat: past_timecode
539
+ }
540
+ token = make_hs256_token(payload, 'bad_secret')
541
+ expect do
542
+ # validator is configured to use "CLIENT_SECRET" by default
543
+ jwt_validator.verify(token)
544
+ end.to raise_error(an_instance_of(JWT::VerificationError))
545
+ end
546
+
381
547
  it 'should verify a valid HS256 token with multiple audiences' do
382
548
  audience = [
383
549
  client_id,
@@ -418,13 +584,44 @@ describe OmniAuth::Auth0::JWTValidator do
418
584
  exp: future_timecode,
419
585
  iss: "https://#{domain}/",
420
586
  iat: past_timecode,
421
- aud: client_id,
422
- kid: jwks_kid
587
+ aud: client_id
423
588
  }
424
589
  token = make_rs256_token(payload)
425
590
  verified_token = make_jwt_validator(opt_domain: domain).verify(token)
426
591
  expect(verified_token['sub']).to eq(sub)
427
592
  end
593
+
594
+ it 'should verify a HS256 JWT signature when calling verify signature directly' do
595
+ sub = 'abc123'
596
+ payload = {
597
+ iss: "https://#{domain}/",
598
+ sub: sub,
599
+ aud: client_id,
600
+ exp: future_timecode,
601
+ iat: past_timecode
602
+ }
603
+ token = make_hs256_token(payload)
604
+ verified_token_signature = jwt_validator.verify_signature(token)
605
+ expect(verified_token_signature[0]).to eq('CLIENT_SECRET')
606
+ expect(verified_token_signature[1]).to eq('HS256')
607
+ end
608
+
609
+ it 'should verify a RS256 JWT signature verify signature directly' do
610
+ domain = 'example.org'
611
+ sub = 'abc123'
612
+ payload = {
613
+ sub: sub,
614
+ exp: future_timecode,
615
+ iss: "https://#{domain}/",
616
+ iat: past_timecode,
617
+ aud: client_id
618
+ }
619
+ token = make_rs256_token(payload)
620
+ verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token)
621
+ expect(verified_token_signature.length).to be(2)
622
+ expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA)
623
+ expect(verified_token_signature[1]).to eq('RS256')
624
+ end
428
625
  end
429
626
 
430
627
  private
@@ -440,14 +637,16 @@ describe OmniAuth::Auth0::JWTValidator do
440
637
  OmniAuth::Auth0::JWTValidator.new(opts)
441
638
  end
442
639
 
443
- def make_hs256_token(payload = nil)
640
+ def make_hs256_token(payload = nil, secret = nil)
444
641
  payload = { sub: 'abc123' } if payload.nil?
445
- JWT.encode payload, client_secret, 'HS256'
642
+ secret = client_secret if secret.nil?
643
+ JWT.encode payload, secret, 'HS256'
446
644
  end
447
645
 
448
- def make_rs256_token(payload = nil)
646
+ def make_rs256_token(payload = nil, kid = nil)
449
647
  payload = { sub: 'abc123' } if payload.nil?
450
- JWT.encode payload, rsa_private_key, 'RS256', kid: jwks_kid
648
+ kid = valid_jwks_kid if kid.nil?
649
+ JWT.encode payload, rsa_private_key, 'RS256', kid: kid
451
650
  end
452
651
 
453
652
  def make_cert(private_key)
@@ -475,7 +674,7 @@ describe OmniAuth::Auth0::JWTValidator do
475
674
  cert.sign private_key, OpenSSL::Digest::SHA1.new
476
675
  end
477
676
 
478
- def stub_jwks
677
+ def stub_complete_jwks
479
678
  stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json')
480
679
  .to_return(
481
680
  headers: { 'Content-Type' => 'application/json' },
@@ -484,18 +683,11 @@ describe OmniAuth::Auth0::JWTValidator do
484
683
  )
485
684
  end
486
685
 
487
- def stub_bad_jwks
488
- stub_request(:get, 'https://samples.auth0.com/.well-known/jwks-bad.json')
489
- .to_return(
490
- status: 404
491
- )
492
- end
493
-
494
- def stub_dummy_jwks
686
+ def stub_expected_jwks
495
687
  stub_request(:get, 'https://example.org/.well-known/jwks.json')
496
688
  .to_return(
497
689
  headers: { 'Content-Type' => 'application/json' },
498
- body: rsa_token_jwks,
690
+ body: valid_jwks,
499
691
  status: 200
500
692
  )
501
693
  end