omniauth-auth0 2.3.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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