omniauth-auth0 2.3.0 → 2.5.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 -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
|