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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +27 -11
- data/.github/CODEOWNERS +1 -1
- data/.github/ISSUE_TEMPLATE/config.yml +8 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +39 -0
- data/.github/ISSUE_TEMPLATE/report_a_bug.md +55 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- data/.gitignore +2 -0
- data/CHANGELOG.md +53 -2
- data/Gemfile +1 -1
- data/README.md +21 -5
- data/lib/omniauth-auth0/version.rb +1 -1
- data/lib/omniauth/auth0/jwt_validator.rb +51 -13
- data/lib/omniauth/strategies/auth0.rb +19 -8
- data/omniauth-auth0.gemspec +2 -3
- data/spec/omniauth/auth0/jwt_validator_spec.rb +226 -34
- data/spec/omniauth/strategies/auth0_spec.rb +49 -17
- metadata +27 -12
- data/.github/ISSUE_TEMPLATE.md +0 -39
- data/Gemfile.lock +0 -168
@@ -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
|
-
|
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
|
-
|
88
|
-
|
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
|
-
|
136
|
-
|
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
|
data/omniauth-auth0.gemspec
CHANGED
@@ -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'
|
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(:
|
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(:
|
21
|
+
let(:valid_jwks) do
|
23
22
|
{
|
24
23
|
keys: [
|
25
24
|
{
|
26
|
-
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
|
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
|
-
|
100
|
+
stub_complete_jwks
|
102
101
|
end
|
103
102
|
|
104
103
|
it 'should return a key' do
|
105
|
-
expect(jwt_validator.jwks_key(:alg,
|
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,
|
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,
|
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, "#{
|
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
|
-
|
138
|
-
make_jwt_validator(opt_issuer:
|
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
|
-
|
142
|
-
|
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
|
-
|
146
|
-
|
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
|
-
|
158
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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:
|
690
|
+
body: valid_jwks,
|
499
691
|
status: 200
|
500
692
|
)
|
501
693
|
end
|