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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +27 -5
- 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/.gitignore +2 -0
- data/CHANGELOG.md +70 -0
- data/Gemfile +1 -1
- data/README.md +99 -2
- data/lib/omniauth-auth0/version.rb +1 -1
- data/lib/omniauth/auth0/jwt_validator.rb +63 -13
- data/lib/omniauth/strategies/auth0.rb +17 -7
- data/omniauth-auth0.gemspec +3 -2
- data/spec/omniauth/auth0/jwt_validator_spec.rb +261 -33
- data/spec/omniauth/strategies/auth0_spec.rb +74 -17
- metadata +26 -11
- data/.github/ISSUE_TEMPLATE.md +0 -39
- data/Gemfile.lock +0 -167
@@ -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
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
137
|
-
|
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
|
data/omniauth-auth0.gemspec
CHANGED
@@ -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
|
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'
|
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(:
|
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(:
|
21
|
+
let(:valid_jwks) do
|
22
22
|
{
|
23
23
|
keys: [
|
24
24
|
{
|
25
|
-
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
|
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
|
-
|
100
|
+
stub_complete_jwks
|
101
101
|
end
|
102
102
|
|
103
103
|
it 'should return a key' do
|
104
|
-
expect(jwt_validator.jwks_key(:alg,
|
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,
|
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,
|
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, "#{
|
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
|
-
|
137
|
-
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
|
138
146
|
end
|
139
147
|
|
140
|
-
|
141
|
-
|
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
|
-
|
145
|
-
|
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
|
-
|
157
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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:
|
725
|
+
body: valid_jwks,
|
498
726
|
status: 200
|
499
727
|
)
|
500
728
|
end
|