omniauth-auth0 2.4.0 → 3.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.
@@ -57,8 +57,7 @@ module OmniAuth
57
57
  auth_scope = session_authorize_params[:scope]
58
58
  if auth_scope.respond_to?(:include?) && auth_scope.include?('openid')
59
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)
60
+ jwt_validator.verify(credentials['id_token'], session_authorize_params)
62
61
  end
63
62
 
64
63
  credentials
@@ -85,9 +84,8 @@ module OmniAuth
85
84
  # Define the parameters used for the /authorize endpoint
86
85
  def authorize_params
87
86
  params = super
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)
87
+ %w[connection connection_scope prompt screen_hint login_hint organization invitation].each do |key|
88
+ params[key] = request.params[key] if request.params.key?(key)
91
89
  end
92
90
 
93
91
  # Generate nonce
@@ -130,11 +128,23 @@ module OmniAuth
130
128
  end
131
129
 
132
130
  private
131
+ def jwt_validator
132
+ @jwt_validator ||= OmniAuth::Auth0::JWTValidator.new(options)
133
+ end
133
134
 
134
135
  # Parse the raw user info.
135
136
  def raw_info
136
- userinfo_url = options.client_options.userinfo_url
137
- @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
138
148
  end
139
149
 
140
150
  # Check if the options include a client_id
@@ -21,9 +21,10 @@ omniauth-auth0 is the OmniAuth strategy for Auth0.
21
21
  s.executables = `git ls-files -- bin/*`.split('\n').map{ |f| File.basename(f) }
22
22
  s.require_paths = ['lib']
23
23
 
24
- s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5'
24
+ s.add_runtime_dependency 'omniauth', '~> 2.0'
25
+ s.add_runtime_dependency 'omniauth-oauth2', '~> 1.7'
25
26
 
26
- s.add_development_dependency 'bundler', '~> 1.9'
27
+ s.add_development_dependency 'bundler'
27
28
 
28
29
  s.license = 'MIT'
29
30
  end
@@ -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(:jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
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(:rsa_token_jwks) do
21
+ let(:valid_jwks) do
22
22
  {
23
23
  keys: [
24
24
  {
25
- kid: jwks_kid,
25
+ kid: valid_jwks_kid,
26
26
  x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)]
27
27
  }
28
28
  ]
@@ -91,29 +91,29 @@ describe OmniAuth::Auth0::JWTValidator do
91
91
  end
92
92
  end
93
93
 
94
- describe 'JWT verifier jwks_key' do
94
+ describe 'JWT verifier jwks key parsing' do
95
95
  let(:jwt_validator) do
96
96
  make_jwt_validator
97
97
  end
98
98
 
99
99
  before do
100
- stub_jwks
100
+ stub_complete_jwks
101
101
  end
102
102
 
103
103
  it 'should return a key' do
104
- expect(jwt_validator.jwks_key(:alg, jwks_kid)).to eq('RS256')
104
+ expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256')
105
105
  end
106
106
 
107
107
  it 'should return an x5c key' do
108
- 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)
109
109
  end
110
110
 
111
111
  it 'should return nil if there is not key' do
112
- expect(jwt_validator.jwks_key(:auth0, jwks_kid)).to eq(nil)
112
+ expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil)
113
113
  end
114
114
 
115
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)
116
+ expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil)
117
117
  end
118
118
  end
119
119
 
@@ -133,16 +133,26 @@ describe OmniAuth::Auth0::JWTValidator do
133
133
  end
134
134
 
135
135
  context 'different from domain' do
136
- let(:jwt_validator) do
137
- 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
138
146
  end
139
147
 
140
- it 'should have the correct issuer' do
141
- 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'
142
151
  end
143
152
 
144
- it 'should have the correct domain' do
145
- 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'
146
156
  end
147
157
  end
148
158
  end
@@ -153,8 +163,24 @@ describe OmniAuth::Auth0::JWTValidator do
153
163
  end
154
164
 
155
165
  before do
156
- stub_jwks
157
- 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
+ }))
158
184
  end
159
185
 
160
186
  it 'should fail with missing issuer' do
@@ -248,6 +274,39 @@ describe OmniAuth::Auth0::JWTValidator do
248
274
  }))
249
275
  end
250
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
+
251
310
  it 'should fail when missing iat' do
252
311
  payload = {
253
312
  iss: "https://#{domain}/",
@@ -377,6 +436,149 @@ describe OmniAuth::Auth0::JWTValidator do
377
436
  }))
378
437
  end
379
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 when authorize params has organization but org_id is missing in the token', focus: true do
480
+ payload = {
481
+ iss: "https://#{domain}/",
482
+ sub: 'sub',
483
+ aud: client_id,
484
+ exp: future_timecode,
485
+ iat: past_timecode
486
+ }
487
+
488
+ token = make_hs256_token(payload)
489
+ expect do
490
+ jwt_validator.verify(token, { organization: 'Test Org' })
491
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
492
+ message: "Organization Id (org_id) claim must be a string present in the ID token"
493
+ }))
494
+ end
495
+
496
+ it 'should fail when authorize params has organization but token org_id does not match', focus: true do
497
+ payload = {
498
+ iss: "https://#{domain}/",
499
+ sub: 'sub',
500
+ aud: client_id,
501
+ exp: future_timecode,
502
+ iat: past_timecode,
503
+ org_id: 'Wrong Org'
504
+ }
505
+
506
+ token = make_hs256_token(payload)
507
+ expect do
508
+ jwt_validator.verify(token, { organization: 'Test Org' })
509
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
510
+ message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'Test Org', found 'Wrong Org'"
511
+ }))
512
+ end
513
+
514
+ it 'should fail for RS256 token when kid is incorrect' do
515
+ domain = 'example.org'
516
+ sub = 'abc123'
517
+ payload = {
518
+ sub: sub,
519
+ exp: future_timecode,
520
+ iss: "https://#{domain}/",
521
+ iat: past_timecode,
522
+ aud: client_id
523
+ }
524
+ invalid_kid = 'invalid-kid'
525
+ token = make_rs256_token(payload, invalid_kid)
526
+ expect do
527
+ verified_token = make_jwt_validator(opt_domain: domain).verify(token)
528
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
529
+ message: "Could not find a public key for Key ID (kid) 'invalid-kid'"
530
+ }))
531
+ end
532
+
533
+ it 'should fail when RS256 token has invalid signature' do
534
+ domain = 'example.org'
535
+ sub = 'abc123'
536
+ payload = {
537
+ sub: sub,
538
+ exp: future_timecode,
539
+ iss: "https://#{domain}/",
540
+ iat: past_timecode,
541
+ aud: client_id
542
+ }
543
+ token = make_rs256_token(payload) + 'bad'
544
+ expect do
545
+ verified_token = make_jwt_validator(opt_domain: domain).verify(token)
546
+ end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({
547
+ message: "Signature verification raised"
548
+ }))
549
+ end
550
+
551
+ it 'should fail when algorithm is not RS256 or HS256' do
552
+ payload = {
553
+ iss: "https://#{domain}/",
554
+ sub: 'abc123',
555
+ aud: client_id,
556
+ exp: future_timecode,
557
+ iat: past_timecode
558
+ }
559
+ token = JWT.encode payload, 'secret', 'HS384'
560
+ expect do
561
+ jwt_validator.verify(token)
562
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
563
+ message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256"
564
+ }))
565
+ end
566
+
567
+ it 'should fail when HS256 token has invalid signature' do
568
+ payload = {
569
+ iss: "https://#{domain}/",
570
+ sub: 'abc123',
571
+ aud: client_id,
572
+ exp: future_timecode,
573
+ iat: past_timecode
574
+ }
575
+ token = make_hs256_token(payload, 'bad_secret')
576
+ expect do
577
+ # validator is configured to use "CLIENT_SECRET" by default
578
+ jwt_validator.verify(token)
579
+ end.to raise_error(an_instance_of(JWT::VerificationError))
580
+ end
581
+
380
582
  it 'should verify a valid HS256 token with multiple audiences' do
381
583
  audience = [
382
584
  client_id,
@@ -417,13 +619,44 @@ describe OmniAuth::Auth0::JWTValidator do
417
619
  exp: future_timecode,
418
620
  iss: "https://#{domain}/",
419
621
  iat: past_timecode,
420
- aud: client_id,
421
- kid: jwks_kid
622
+ aud: client_id
422
623
  }
423
624
  token = make_rs256_token(payload)
424
625
  verified_token = make_jwt_validator(opt_domain: domain).verify(token)
425
626
  expect(verified_token['sub']).to eq(sub)
426
627
  end
628
+
629
+ it 'should verify a HS256 JWT signature when calling verify signature directly' do
630
+ sub = 'abc123'
631
+ payload = {
632
+ iss: "https://#{domain}/",
633
+ sub: sub,
634
+ aud: client_id,
635
+ exp: future_timecode,
636
+ iat: past_timecode
637
+ }
638
+ token = make_hs256_token(payload)
639
+ verified_token_signature = jwt_validator.verify_signature(token)
640
+ expect(verified_token_signature[0]).to eq('CLIENT_SECRET')
641
+ expect(verified_token_signature[1]).to eq('HS256')
642
+ end
643
+
644
+ it 'should verify a RS256 JWT signature verify signature directly' do
645
+ domain = 'example.org'
646
+ sub = 'abc123'
647
+ payload = {
648
+ sub: sub,
649
+ exp: future_timecode,
650
+ iss: "https://#{domain}/",
651
+ iat: past_timecode,
652
+ aud: client_id
653
+ }
654
+ token = make_rs256_token(payload)
655
+ verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token)
656
+ expect(verified_token_signature.length).to be(2)
657
+ expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA)
658
+ expect(verified_token_signature[1]).to eq('RS256')
659
+ end
427
660
  end
428
661
 
429
662
  private
@@ -439,14 +672,16 @@ describe OmniAuth::Auth0::JWTValidator do
439
672
  OmniAuth::Auth0::JWTValidator.new(opts)
440
673
  end
441
674
 
442
- def make_hs256_token(payload = nil)
675
+ def make_hs256_token(payload = nil, secret = nil)
443
676
  payload = { sub: 'abc123' } if payload.nil?
444
- JWT.encode payload, client_secret, 'HS256'
677
+ secret = client_secret if secret.nil?
678
+ JWT.encode payload, secret, 'HS256'
445
679
  end
446
680
 
447
- def make_rs256_token(payload = nil)
681
+ def make_rs256_token(payload = nil, kid = nil)
448
682
  payload = { sub: 'abc123' } if payload.nil?
449
- JWT.encode payload, rsa_private_key, 'RS256', kid: jwks_kid
683
+ kid = valid_jwks_kid if kid.nil?
684
+ JWT.encode payload, rsa_private_key, 'RS256', kid: kid
450
685
  end
451
686
 
452
687
  def make_cert(private_key)
@@ -474,7 +709,7 @@ describe OmniAuth::Auth0::JWTValidator do
474
709
  cert.sign private_key, OpenSSL::Digest::SHA1.new
475
710
  end
476
711
 
477
- def stub_jwks
712
+ def stub_complete_jwks
478
713
  stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json')
479
714
  .to_return(
480
715
  headers: { 'Content-Type' => 'application/json' },
@@ -483,18 +718,11 @@ describe OmniAuth::Auth0::JWTValidator do
483
718
  )
484
719
  end
485
720
 
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
721
+ def stub_expected_jwks
494
722
  stub_request(:get, 'https://example.org/.well-known/jwks.json')
495
723
  .to_return(
496
724
  headers: { 'Content-Type' => 'application/json' },
497
- body: rsa_token_jwks,
725
+ body: valid_jwks,
498
726
  status: 200
499
727
  )
500
728
  end