omniauth-auth0 2.3.1 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,16 +84,15 @@ 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 prompt].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
94
92
  params[:nonce] = SecureRandom.hex
95
93
  # Generate leeway if none exists
96
94
  params[:leeway] = 60 unless params[:leeway]
97
-
95
+
98
96
  # Store authorize params in the session for token verification
99
97
  session['authorize_params'] = params
100
98
 
@@ -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', '~> 1.9'
24
25
  s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5'
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