omniauth-auth0 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of omniauth-auth0 might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 463fae0687e0473a0918c9c2086f3b47a60ae1448fffa3b1157ec933784c1a1c
4
- data.tar.gz: d872be3b458dadf3752d58192059d6d350a90f7047de1e84b98137417880204d
3
+ metadata.gz: 07f73634dc123aa4d6912e4d17e7c461948cf6b1fa2086003600434bfd99dcf9
4
+ data.tar.gz: 29e79d38181335c08618108b96e667d24c768185432e2d5e22312e716c333b48
5
5
  SHA512:
6
- metadata.gz: 13efc37572c71bdd5184dd9888e48f2479e5e5d52c16f454c01c88834fc2f5dffa3488b13b8b0dc9bbe423d9aa750038c12693f85e83f3375482af8857788585
7
- data.tar.gz: 2240535e4f749e7ba47f587ca56ae9e1d5ee596e54391a969a5f282b852ab8f3a2970c99514014fada2d5a00b259144849e6e2db1ae559a8ab584753e4e7bce9
6
+ metadata.gz: 0d1fa93a13a96f24a55ea4fa475561d7252cfd39a2975277ca93be8a91b07a58222dd16e522225f1e4bf0ff3ad5a7456bdac6e640e2e372f3a415ca6b4fafe7a
7
+ data.tar.gz: 6528f8a41f91bffb7a8c6285882a0f9179c51a3bc133824f896cbae7d36f7deafa2d70bf6f7b20bdb684c66831d043f91c3c9f938cc21d603da4d7788a4cca1d
data/.circleci/config.yml CHANGED
@@ -15,35 +15,14 @@ jobs:
15
15
  paths:
16
16
  - Gemfile
17
17
  - Gemfile.lock
18
+ - .snyk
18
19
  - save_cache:
19
20
  key: gems-v2--{{ checksum "Gemfile.lock" }}
20
21
  paths:
21
22
  - vendor/bundle
22
23
  - run: bundle exec rake spec
23
- snyk:
24
- docker:
25
- - image: snyk/snyk-cli:rubygems
26
- steps:
27
- - attach_workspace:
28
- at: .
29
- - run: snyk test
30
- - run:
31
- command: |
32
- if [[ "${CIRCLE_BRANCH}" == "master" ]]
33
- then
34
- snyk monitor --org=auth0-sdks
35
- fi
36
- when: always
37
24
 
38
25
  workflows:
39
26
  tests:
40
27
  jobs:
41
28
  - run-tests
42
- snyk:
43
- jobs:
44
- - run-tests
45
- - snyk:
46
- # Must define SNYK_TOKEN env
47
- context: snyk-env
48
- requires:
49
- - run-tests
@@ -0,0 +1 @@
1
+ * @auth0/dx-sdks-approver
data/.github/stale.yml ADDED
@@ -0,0 +1,20 @@
1
+ # Configuration for probot-stale - https://github.com/probot/stale
2
+
3
+ # Number of days of inactivity before an Issue or Pull Request becomes stale
4
+ daysUntilStale: 90
5
+
6
+ # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
7
+ daysUntilClose: 7
8
+
9
+ # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
10
+ exemptLabels: []
11
+
12
+ # Set to true to ignore issues with an assignee (defaults to false)
13
+ exemptAssignees: true
14
+
15
+ # Label to use when marking as stale
16
+ staleLabel: closed:stale
17
+
18
+ # Comment to post when marking as stale. Set to `false` to disable
19
+ markComment: >
20
+ This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️
data/.snyk ADDED
@@ -0,0 +1,9 @@
1
+ # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2
+ version: v1.13.5
3
+ # ignores vulnerabilities until expiry date; change duration by modifying expiry date
4
+ ignore:
5
+ SNYK-RUBY-OMNIAUTH-174820:
6
+ - '*':
7
+ reason: Not affected.
8
+ expires: 2020-01-01T00:00:00.000Z
9
+ patch: {}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Change Log
2
2
 
3
+ ## [v2.3.0](https://github.com/auth0/omniauth-auth0/tree/v2.3.0) (2020-03-06)
4
+ [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.3.0...v2.2.0)
5
+
6
+ **Added**
7
+ - Improved OIDC Compliance [\#74](https://github.com/auth0/omniauth-auth0/pull/92) ([davidpatrick](https://github.com/davidpatrick))
8
+
3
9
  ## [v2.2.0](https://github.com/auth0/omniauth-auth0/tree/v2.2.0) (2018-04-18)
4
10
  [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.1.0...v2.2.0)
5
11
 
data/Gemfile.lock CHANGED
@@ -1,16 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- omniauth-auth0 (2.2.0)
4
+ omniauth-auth0 (2.3.0)
5
5
  omniauth-oauth2 (~> 1.5)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- addressable (2.6.0)
11
- public_suffix (>= 2.0.2, < 4.0)
10
+ addressable (2.7.0)
11
+ public_suffix (>= 2.0.2, < 5.0)
12
12
  ast (2.4.0)
13
- codecov (0.1.14)
13
+ codecov (0.1.16)
14
14
  json
15
15
  simplecov
16
16
  url
@@ -19,15 +19,15 @@ GEM
19
19
  safe_yaml (~> 1.0.0)
20
20
  daemons (1.3.1)
21
21
  diff-lcs (1.3)
22
- docile (1.3.1)
23
- dotenv (2.7.2)
22
+ docile (1.3.2)
23
+ dotenv (2.7.5)
24
24
  eventmachine (1.2.7)
25
- faraday (0.15.4)
25
+ faraday (1.0.0)
26
26
  multipart-post (>= 1.2, < 3)
27
- ffi (1.10.0)
27
+ ffi (1.12.2)
28
28
  formatador (0.2.5)
29
- gem-release (2.0.1)
30
- guard (2.15.0)
29
+ gem-release (2.1.1)
30
+ guard (2.16.1)
31
31
  formatador (>= 0.2.4)
32
32
  listen (>= 2.7, < 4.0)
33
33
  lumberjack (>= 1.0.12, < 2.0)
@@ -41,104 +41,105 @@ GEM
41
41
  guard (~> 2.1)
42
42
  guard-compat (~> 1.1)
43
43
  rspec (>= 2.99.0, < 4.0)
44
- hashdiff (0.3.8)
45
- hashie (3.6.0)
46
- jaro_winkler (1.5.2)
47
- json (2.2.0)
48
- jwt (2.1.0)
44
+ hashdiff (1.0.1)
45
+ hashie (4.1.0)
46
+ jaro_winkler (1.5.4)
47
+ json (2.3.0)
48
+ jwt (2.2.1)
49
49
  listen (3.1.5)
50
50
  rb-fsevent (~> 0.9, >= 0.9.4)
51
51
  rb-inotify (~> 0.9, >= 0.9.7)
52
52
  ruby_dep (~> 1.2)
53
- lumberjack (1.0.13)
53
+ lumberjack (1.2.4)
54
54
  method_source (0.9.2)
55
- multi_json (1.13.1)
55
+ multi_json (1.14.1)
56
56
  multi_xml (0.6.0)
57
- multipart-post (2.0.0)
58
- mustermann (1.0.3)
57
+ multipart-post (2.1.1)
58
+ mustermann (1.1.1)
59
+ ruby2_keywords (~> 0.0.1)
59
60
  nenv (0.3.0)
60
- notiffany (0.1.1)
61
+ notiffany (0.1.3)
61
62
  nenv (~> 0.1)
62
63
  shellany (~> 0.0)
63
- oauth2 (1.4.1)
64
- faraday (>= 0.8, < 0.16.0)
64
+ oauth2 (1.4.4)
65
+ faraday (>= 0.8, < 2.0)
65
66
  jwt (>= 1.0, < 3.0)
66
67
  multi_json (~> 1.3)
67
68
  multi_xml (~> 0.5)
68
69
  rack (>= 1.2, < 3)
69
- omniauth (1.9.0)
70
- hashie (>= 3.4.6, < 3.7.0)
70
+ omniauth (1.9.1)
71
+ hashie (>= 3.4.6)
71
72
  rack (>= 1.6.2, < 3)
72
73
  omniauth-oauth2 (1.6.0)
73
74
  oauth2 (~> 1.1)
74
75
  omniauth (~> 1.9)
75
- parallel (1.17.0)
76
- parser (2.6.2.1)
76
+ parallel (1.19.1)
77
+ parser (2.7.0.4)
77
78
  ast (~> 2.4.0)
78
79
  pry (0.12.2)
79
80
  coderay (~> 1.1.0)
80
81
  method_source (~> 0.9.0)
81
- psych (3.1.0)
82
- public_suffix (3.0.3)
83
- rack (2.0.7)
84
- rack-protection (2.0.5)
82
+ public_suffix (4.0.3)
83
+ rack (2.2.2)
84
+ rack-protection (2.0.8.1)
85
85
  rack
86
86
  rack-test (1.1.0)
87
87
  rack (>= 1.0, < 3)
88
88
  rainbow (3.0.0)
89
- rake (12.3.2)
89
+ rake (13.0.1)
90
90
  rb-fsevent (0.10.3)
91
- rb-inotify (0.10.0)
91
+ rb-inotify (0.10.1)
92
92
  ffi (~> 1.0)
93
- rspec (3.8.0)
94
- rspec-core (~> 3.8.0)
95
- rspec-expectations (~> 3.8.0)
96
- rspec-mocks (~> 3.8.0)
97
- rspec-core (3.8.0)
98
- rspec-support (~> 3.8.0)
99
- rspec-expectations (3.8.2)
93
+ rexml (3.2.4)
94
+ rspec (3.9.0)
95
+ rspec-core (~> 3.9.0)
96
+ rspec-expectations (~> 3.9.0)
97
+ rspec-mocks (~> 3.9.0)
98
+ rspec-core (3.9.1)
99
+ rspec-support (~> 3.9.1)
100
+ rspec-expectations (3.9.0)
100
101
  diff-lcs (>= 1.2.0, < 2.0)
101
- rspec-support (~> 3.8.0)
102
- rspec-mocks (3.8.0)
102
+ rspec-support (~> 3.9.0)
103
+ rspec-mocks (3.9.1)
103
104
  diff-lcs (>= 1.2.0, < 2.0)
104
- rspec-support (~> 3.8.0)
105
- rspec-support (3.8.0)
106
- rubocop (0.67.2)
105
+ rspec-support (~> 3.9.0)
106
+ rspec-support (3.9.2)
107
+ rubocop (0.80.1)
107
108
  jaro_winkler (~> 1.5.1)
108
109
  parallel (~> 1.10)
109
- parser (>= 2.5, != 2.5.1.1)
110
- psych (>= 3.1.0)
110
+ parser (>= 2.7.0.1)
111
111
  rainbow (>= 2.2.2, < 4.0)
112
+ rexml
112
113
  ruby-progressbar (~> 1.7)
113
- unicode-display_width (>= 1.4.0, < 1.6)
114
- ruby-progressbar (1.10.0)
114
+ unicode-display_width (>= 1.4.0, < 1.7)
115
+ ruby-progressbar (1.10.1)
116
+ ruby2_keywords (0.0.2)
115
117
  ruby_dep (1.5.0)
116
118
  safe_yaml (1.0.5)
117
119
  shellany (0.0.1)
118
120
  shotgun (0.9.2)
119
121
  rack (>= 1.0)
120
- simplecov (0.16.1)
122
+ simplecov (0.18.5)
121
123
  docile (~> 1.1)
122
- json (>= 1.8, < 3)
123
- simplecov-html (~> 0.10.0)
124
- simplecov-html (0.10.2)
125
- sinatra (2.0.5)
124
+ simplecov-html (~> 0.11)
125
+ simplecov-html (0.12.2)
126
+ sinatra (2.0.8.1)
126
127
  mustermann (~> 1.0)
127
128
  rack (~> 2.0)
128
- rack-protection (= 2.0.5)
129
+ rack-protection (= 2.0.8.1)
129
130
  tilt (~> 2.0)
130
131
  thin (1.7.2)
131
132
  daemons (~> 1.0, >= 1.0.9)
132
133
  eventmachine (~> 1.0, >= 1.0.4)
133
134
  rack (>= 1, < 3)
134
- thor (0.20.3)
135
- tilt (2.0.9)
136
- unicode-display_width (1.5.0)
135
+ thor (1.0.1)
136
+ tilt (2.0.10)
137
+ unicode-display_width (1.6.1)
137
138
  url (0.3.2)
138
- webmock (3.5.1)
139
+ webmock (3.8.2)
139
140
  addressable (>= 2.3.6)
140
141
  crack (>= 0.3.2)
141
- hashdiff
142
+ hashdiff (>= 0.4.0, < 2.0.0)
142
143
 
143
144
  PLATFORMS
144
145
  ruby
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  An [OmniAuth](https://github.com/intridea/omniauth) strategy for authenticating with [Auth0](https://auth0.com). This strategy is based on the [OmniAuth OAuth2](https://github.com/omniauth/omniauth-oauth2) strategy.
4
4
 
5
+ **Important security note:** The parent library for this strategy currently has an unresolved security issue. Please see the discussion, including mitigations for Rails and non-Rails applications, [here](https://github.com/auth0/omniauth-auth0/issues/82).
6
+
5
7
  [![CircleCI](https://img.shields.io/circleci/project/github/auth0/omniauth-auth0/master.svg)](https://circleci.com/gh/auth0/omniauth-auth0)
6
8
  [![codecov](https://codecov.io/gh/auth0/omniauth-auth0/branch/master/graph/badge.svg)](https://codecov.io/gh/auth0/omniauth-auth0)
7
9
  [![Gem Version](https://badge.fury.io/rb/omniauth-auth0.svg)](https://badge.fury.io/rb/omniauth-auth0)
@@ -31,6 +33,12 @@ Add the following line to your `Gemfile`:
31
33
  gem 'omniauth-auth0'
32
34
  ```
33
35
 
36
+ If you're using this strategy with Rails, also add the following for CSRF protection:
37
+
38
+ ```ruby
39
+ gem 'omniauth-rails_csrf_protection'
40
+ ```
41
+
34
42
  Then install:
35
43
 
36
44
  ```bash
@@ -63,19 +71,13 @@ provider
63
71
  {
64
72
  authorize_params: {
65
73
  scope: 'openid read:users write:order',
66
- audience: 'https://mydomain/api'
74
+ audience: 'https://mydomain/api',
75
+ max_age: 3600 # time in seconds authentication is valid
67
76
  }
68
77
  }
69
78
  ```
70
79
 
71
- ... which will tell the strategy to send those parameters on every Auth request.
72
-
73
- Or you can do it for a specific authentication request by adding them to the query parameters of the redirect URL. Allowed parameters are `connection` and `prompt`:
74
-
75
- ```ruby
76
- redirect_to '/auth/auth0?connection=google-oauth2'
77
- redirect_to '/auth/auth0?prompt=none'
78
- ```
80
+ ... which will tell the strategy to send those parameters on every authentication request.
79
81
 
80
82
  ### Authentication hash
81
83
 
@@ -129,7 +131,6 @@ We appreciate feedback and contribution to this repo! Before you get started, pl
129
131
 
130
132
  ## Support + Feedback
131
133
 
132
-
133
134
  - Use [Community](https://community.auth0.com/) for usage, questions, specific cases.
134
135
  - Use [Issues](https://github.com/auth0/omniauth-auth0/issues) here for code-level support and bug reports.
135
136
  - Paid customers can use [Support](https://support.auth0.com/) to submit a trouble ticket for production-affecting issues.
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module Auth0
3
- VERSION = '2.2.0'.freeze
3
+ VERSION = '2.3.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,11 @@
1
+ module OmniAuth
2
+ module Auth0
3
+ class TokenValidationError < StandardError
4
+ attr_reader :error_reason
5
+ def initialize(msg)
6
+ @error_reason = msg
7
+ super(msg)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -15,7 +15,8 @@ module OmniAuth
15
15
  # options.issuer - Application issuer (optional).
16
16
  # options.client_id - Application Client ID.
17
17
  # options.client_secret - Application Client Secret.
18
- def initialize(options)
18
+
19
+ def initialize(options, authorize_params = {})
19
20
  @domain = uri_string(options.domain)
20
21
 
21
22
  # Use custom issuer if provided, otherwise use domain
@@ -26,23 +27,38 @@ module OmniAuth
26
27
  @client_secret = options.client_secret
27
28
  end
28
29
 
29
- # Decode a JWT.
30
- # @param jwt string - JWT to decode.
31
- # @return hash - The decoded token, if there were no exceptions.
32
- # @see https://github.com/jwt/ruby-jwt
33
- def decode(jwt)
30
+ def verify_signature(jwt)
34
31
  head = token_head(jwt)
35
32
 
36
33
  # Make sure the algorithm is supported and get the decode key.
37
- decode_key = @client_secret
38
34
  if head[:alg] == 'RS256'
39
- decode_key = rs256_decode_key(head[:kid])
40
- elsif head[:alg] != 'HS256'
41
- raise JWT::VerificationError, :id_token_alg_unsupported
35
+ [rs256_decode_key(head[:kid]), head[:alg]]
36
+ elsif head[:alg] == 'HS256'
37
+ [@client_secret, head[:alg]]
38
+ else
39
+ raise OmniAuth::Auth0::TokenValidationError.new("Signature algorithm of #{head[:alg]} is not supported. Expected the ID token to be signed with RS256 or HS256")
40
+ end
41
+ end
42
+
43
+ # Verify a JWT.
44
+ # @param jwt string - JWT to verify.
45
+ # @param authorize_params hash - Authorization params to verify on the JWT
46
+ # @return hash - The verified token, if there were no exceptions.
47
+ def verify(jwt, authorize_params = {})
48
+ if !jwt
49
+ raise OmniAuth::Auth0::TokenValidationError.new('ID token is required but missing')
50
+ end
51
+
52
+ parts = jwt.split('.')
53
+ if parts.length != 3
54
+ raise OmniAuth::Auth0::TokenValidationError.new('ID token could not be decoded')
42
55
  end
43
56
 
44
- # Docs: https://github.com/jwt/ruby-jwt#algorithms-and-usage
45
- JWT.decode(jwt, decode_key, true, decode_opts(head[:alg]))
57
+ key, alg = verify_signature(jwt)
58
+ id_token, header = JWT.decode(jwt, key, false)
59
+ verify_claims(id_token, authorize_params)
60
+
61
+ return id_token
46
62
  end
47
63
 
48
64
  # Get the decoded head segment from a JWT.
@@ -76,26 +92,12 @@ module OmniAuth
76
92
  end
77
93
 
78
94
  private
79
-
80
- # Get the JWT decode options
81
- # Docs: https://github.com/jwt/ruby-jwt#add-custom-header-fields
82
- # @return hash
83
- def decode_opts(alg)
84
- {
85
- algorithm: alg,
86
- leeway: 30,
87
- verify_expiration: true,
88
- verify_iss: true,
89
- iss: @issuer,
90
- verify_aud: true,
91
- aud: @client_id,
92
- verify_not_before: true
93
- }
94
- end
95
-
96
95
  def rs256_decode_key(kid)
97
96
  jwks_x5c = jwks_key(:x5c, kid)
98
- raise JWT::VerificationError, :jwks_missing_x5c if jwks_x5c.nil?
97
+
98
+ if jwks_x5c.nil?
99
+ raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}''")
100
+ end
99
101
 
100
102
  jwks_public_cert(jwks_x5c.first)
101
103
  end
@@ -129,6 +131,97 @@ module OmniAuth
129
131
  temp_domain = URI("https://#{uri}") unless temp_domain.scheme
130
132
  "#{temp_domain}/"
131
133
  end
134
+
135
+ def verify_claims(id_token, authorize_params)
136
+ leeway = authorize_params[:leeway] || 60
137
+ max_age = authorize_params[:max_age]
138
+ nonce = authorize_params[:nonce]
139
+
140
+ verify_iss(id_token)
141
+ verify_sub(id_token)
142
+ verify_aud(id_token)
143
+ verify_expiration(id_token, leeway)
144
+ verify_iat(id_token)
145
+ verify_nonce(id_token, nonce)
146
+ verify_azp(id_token)
147
+ verify_auth_time(id_token, leeway, max_age)
148
+ end
149
+
150
+ def verify_iss(id_token)
151
+ issuer = id_token['iss']
152
+ if !issuer
153
+ raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim must be a string present in the ID token")
154
+ elsif @issuer != issuer
155
+ raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim mismatch in the ID token, expected (#{@issuer}), found (#{id_token['iss']})")
156
+ end
157
+ end
158
+
159
+ def verify_sub(id_token)
160
+ subject = id_token['sub']
161
+ if !subject || !subject.is_a?(String) || subject.empty?
162
+ raise OmniAuth::Auth0::TokenValidationError.new('Subject (sub) claim must be a string present in the ID token')
163
+ end
164
+ end
165
+
166
+ def verify_aud(id_token)
167
+ audience = id_token['aud']
168
+ if !audience || !(audience.is_a?(String) || audience.is_a?(Array))
169
+ raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim must be a string or array of strings present in the ID token")
170
+ elsif audience.is_a?(Array) && !audience.include?(@client_id)
171
+ raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but was not one of #{audience.join(', ')}")
172
+ elsif audience.is_a?(String) && audience != @client_id
173
+ raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but found #{audience}")
174
+ end
175
+ end
176
+
177
+ def verify_expiration(id_token, leeway)
178
+ expiration = id_token['exp']
179
+ if !expiration || !expiration.is_a?(Integer)
180
+ raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim must be a number present in the ID token")
181
+ elsif expiration <= Time.now.to_i - leeway
182
+ raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(expiration + leeway)})")
183
+ end
184
+ end
185
+
186
+ def verify_iat(id_token)
187
+ if !id_token['iat']
188
+ raise OmniAuth::Auth0::TokenValidationError.new("Issued At (iat) claim must be a number present in the ID token")
189
+ end
190
+ end
191
+
192
+ def verify_nonce(id_token, nonce)
193
+ if nonce
194
+ received_nonce = id_token['nonce']
195
+ if !received_nonce
196
+ raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim must be a string present in the ID token")
197
+ elsif nonce != received_nonce
198
+ raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim value mismatch in the ID token; expected (#{nonce}), found (#{received_nonce})")
199
+ end
200
+ end
201
+ end
202
+
203
+ def verify_azp(id_token)
204
+ audience = id_token['aud']
205
+ if audience.is_a?(Array) && audience.length > 1
206
+ azp = id_token['azp']
207
+ if !azp || !azp.is_a?(String)
208
+ raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values")
209
+ elsif azp != @client_id
210
+ raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim mismatch in the ID token; expected (#{@client_id}), found (#{azp})")
211
+ end
212
+ end
213
+ end
214
+
215
+ def verify_auth_time(id_token, leeway, max_age)
216
+ if max_age
217
+ auth_time = id_token['auth_time']
218
+ if !auth_time || !auth_time.is_a?(Integer)
219
+ raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified")
220
+ elsif Time.now.to_i > auth_time + max_age + leeway;
221
+ raise OmniAuth::Auth0::TokenValidationError.new("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 + leeway)})")
222
+ end
223
+ end
224
+ end
132
225
  end
133
226
  end
134
227
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'base64'
4
4
  require 'uri'
5
+ require 'securerandom'
5
6
  require 'omniauth-oauth2'
6
7
  require 'omniauth/auth0/jwt_validator'
7
8
  require 'omniauth/auth0/telemetry'
@@ -48,10 +49,16 @@ module OmniAuth
48
49
  )
49
50
  end
50
51
 
51
- # Make sure the ID token can be verified and decoded.
52
- auth0_jwt = OmniAuth::Auth0::JWTValidator.new(options)
53
- jwt_decoded = auth0_jwt.decode(credentials['id_token'])
54
- fail!(:invalid_id_token) unless jwt_decoded.length
52
+ # Retrieve and remove authorization params from the session
53
+ session_authorize_params = session['authorize_params'] || {}
54
+ session.delete('authorize_params')
55
+
56
+ auth_scope = session_authorize_params[:scope]
57
+ if auth_scope.respond_to?(:include?) && auth_scope.include?('openid')
58
+ # 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)
61
+ end
55
62
 
56
63
  credentials
57
64
  end
@@ -78,8 +85,18 @@ module OmniAuth
78
85
  def authorize_params
79
86
  params = super
80
87
  parsed_query = Rack::Utils.parse_query(request.query_string)
81
- params['connection'] = parsed_query['connection']
82
- params['prompt'] = parsed_query['prompt']
88
+ %w[connection prompt].each do |key|
89
+ params[key] = parsed_query[key] if parsed_query.key?(key)
90
+ end
91
+
92
+ # Generate nonce
93
+ params[:nonce] = SecureRandom.hex
94
+ # Generate leeway if none exists
95
+ params[:leeway] = 60 unless params[:leeway]
96
+
97
+ # Store authorize params in the session for token verification
98
+ session['authorize_params'] = params
99
+
83
100
  params
84
101
  end
85
102
 
@@ -105,6 +122,12 @@ module OmniAuth
105
122
  end
106
123
  end
107
124
 
125
+ def callback_phase
126
+ super
127
+ rescue OmniAuth::Auth0::TokenValidationError => e
128
+ fail!(:token_validation_error, e)
129
+ end
130
+
108
131
  private
109
132
 
110
133
  # Parse the raw user info.
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
2
  require 'json'
3
3
  require 'jwt'
4
+ require 'omniauth/auth0/errors'
4
5
 
5
6
  describe OmniAuth::Auth0::JWTValidator do
6
7
  #
@@ -147,7 +148,7 @@ describe OmniAuth::Auth0::JWTValidator do
147
148
  end
148
149
  end
149
150
 
150
- describe 'JWT verifier decode' do
151
+ describe 'JWT verifier verify' do
151
152
  let(:jwt_validator) do
152
153
  make_jwt_validator
153
154
  end
@@ -157,91 +158,259 @@ describe OmniAuth::Auth0::JWTValidator do
157
158
  stub_dummy_jwks
158
159
  end
159
160
 
160
- it 'should fail with passed expiration' do
161
+ it 'should fail with missing issuer' do
162
+ expect do
163
+ jwt_validator.verify(make_hs256_token)
164
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
165
+ message: "Issuer (iss) claim must be a string present in the ID token"
166
+ }))
167
+ end
168
+
169
+ it 'should fail with invalid issuer' do
170
+ payload = {
171
+ iss: 'https://auth0.com/'
172
+ }
173
+ token = make_hs256_token(payload)
174
+ expect do
175
+ jwt_validator.verify(token)
176
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
177
+ message: "Issuer (iss) claim mismatch in the ID token, expected (https://samples.auth0.com/), found (https://auth0.com/)"
178
+ }))
179
+ end
180
+
181
+ it 'should fail when subject is missing' do
161
182
  payload = {
183
+ iss: "https://#{domain}/",
184
+ sub: ''
185
+ }
186
+ token = make_hs256_token(payload)
187
+ expect do
188
+ jwt_validator.verify(token)
189
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
190
+ message: "Subject (sub) claim must be a string present in the ID token"
191
+ }))
192
+ end
193
+
194
+ it 'should fail with missing audience' do
195
+ payload = {
196
+ iss: "https://#{domain}/",
197
+ sub: 'sub'
198
+ }
199
+ token = make_hs256_token(payload)
200
+ expect do
201
+ jwt_validator.verify(token)
202
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
203
+ message: "Audience (aud) claim must be a string or array of strings present in the ID token"
204
+ }))
205
+ end
206
+
207
+ it 'should fail with invalid audience' do
208
+ payload = {
209
+ iss: "https://#{domain}/",
210
+ sub: 'sub',
211
+ aud: 'Auth0'
212
+ }
213
+ token = make_hs256_token(payload)
214
+ expect do
215
+ jwt_validator.verify(token)
216
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
217
+ message: "Audience (aud) claim mismatch in the ID token; expected #{client_id} but found Auth0"
218
+ }))
219
+ end
220
+
221
+ it 'should fail when missing expiration' do
222
+ payload = {
223
+ iss: "https://#{domain}/",
224
+ sub: 'sub',
225
+ aud: client_id
226
+ }
227
+
228
+ token = make_hs256_token(payload)
229
+ expect do
230
+ jwt_validator.verify(token)
231
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
232
+ message: "Expiration time (exp) claim must be a number present in the ID token"
233
+ }))
234
+ end
235
+
236
+ it 'should fail when past expiration' do
237
+ payload = {
238
+ iss: "https://#{domain}/",
239
+ sub: 'sub',
240
+ aud: client_id,
162
241
  exp: past_timecode
163
242
  }
243
+
164
244
  token = make_hs256_token(payload)
165
245
  expect do
166
- jwt_validator.decode(token)
167
- end.to raise_error(JWT::ExpiredSignature)
246
+ jwt_validator.verify(token)
247
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
248
+ message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(past_timecode + 60)})"
249
+ }))
168
250
  end
169
251
 
170
- it 'should fail with missing issuer' do
252
+ it 'should fail when missing iat' do
253
+ payload = {
254
+ iss: "https://#{domain}/",
255
+ sub: 'sub',
256
+ aud: client_id,
257
+ exp: future_timecode
258
+ }
259
+
260
+ token = make_hs256_token(payload)
171
261
  expect do
172
- jwt_validator.decode(make_hs256_token)
173
- end.to raise_error(JWT::InvalidIssuerError)
262
+ jwt_validator.verify(token)
263
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
264
+ message: "Issued At (iat) claim must be a number present in the ID token"
265
+ }))
174
266
  end
175
267
 
176
- it 'should fail with invalid issuer' do
268
+ it 'should fail when authorize params has nonce but nonce is missing in the token' do
177
269
  payload = {
178
- iss: 'https://auth0.com/'
270
+ iss: "https://#{domain}/",
271
+ sub: 'sub',
272
+ aud: client_id,
273
+ exp: future_timecode,
274
+ iat: past_timecode
179
275
  }
276
+
180
277
  token = make_hs256_token(payload)
181
278
  expect do
182
- jwt_validator.decode(token)
183
- end.to raise_error(JWT::InvalidIssuerError)
279
+ jwt_validator.verify(token, { nonce: 'noncey' })
280
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
281
+ message: "Nonce (nonce) claim must be a string present in the ID token"
282
+ }))
184
283
  end
185
284
 
186
- it 'should fail with a future not before' do
285
+ it 'should fail when authorize params has nonce but token nonce does not match' do
286
+ payload = {
287
+ iss: "https://#{domain}/",
288
+ sub: 'sub',
289
+ aud: client_id,
290
+ exp: future_timecode,
291
+ iat: past_timecode,
292
+ nonce: 'mismatch'
293
+ }
294
+
295
+ token = make_hs256_token(payload)
296
+ expect do
297
+ jwt_validator.verify(token, { nonce: 'noncey' })
298
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
299
+ message: "Nonce (nonce) claim value mismatch in the ID token; expected (noncey), found (mismatch)"
300
+ }))
301
+ end
302
+
303
+ it 'should fail when “aud” is an array of strings and azp claim is not present' do
304
+ aud = [
305
+ client_id,
306
+ "https://#{domain}/userinfo"
307
+ ]
187
308
  payload = {
188
- nbf: future_timecode,
189
- iss: "https://#{domain}/"
309
+ iss: "https://#{domain}/",
310
+ sub: 'sub',
311
+ aud: aud,
312
+ exp: future_timecode,
313
+ iat: past_timecode
190
314
  }
315
+
191
316
  token = make_hs256_token(payload)
192
317
  expect do
193
- jwt_validator.decode(token)
194
- end.to raise_error(JWT::ImmatureSignature)
318
+ jwt_validator.verify(token)
319
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
320
+ message: "Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values"
321
+ }))
195
322
  end
196
323
 
197
- it 'should fail with missing audience' do
324
+ it 'should fail when "azp" claim doesnt match the expected aud' do
325
+ aud = [
326
+ client_id,
327
+ "https://#{domain}/userinfo"
328
+ ]
329
+ payload = {
330
+ iss: "https://#{domain}/",
331
+ sub: 'sub',
332
+ aud: aud,
333
+ exp: future_timecode,
334
+ iat: past_timecode,
335
+ azp: 'not_expected'
336
+ }
337
+
338
+ token = make_hs256_token(payload)
339
+ expect do
340
+ jwt_validator.verify(token)
341
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
342
+ message: "Authorized Party (azp) claim mismatch in the ID token; expected (#{client_id}), found (not_expected)"
343
+ }))
344
+ end
345
+
346
+ it 'should fail when “max_age” sent on the authentication request and this claim is not present' do
198
347
  payload = {
199
- iss: "https://#{domain}/"
348
+ iss: "https://#{domain}/",
349
+ sub: 'sub',
350
+ aud: client_id,
351
+ exp: future_timecode,
352
+ iat: past_timecode
200
353
  }
354
+
201
355
  token = make_hs256_token(payload)
202
356
  expect do
203
- jwt_validator.decode(token)
204
- end.to raise_error(JWT::InvalidAudError)
357
+ jwt_validator.verify(token, { max_age: 60 })
358
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
359
+ message: "Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified"
360
+ }))
205
361
  end
206
362
 
207
- it 'should fail with invalid audience' do
363
+ 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' do
208
364
  payload = {
209
365
  iss: "https://#{domain}/",
210
- aud: 'Auth0'
366
+ sub: 'sub',
367
+ aud: client_id,
368
+ exp: future_timecode,
369
+ iat: past_timecode,
370
+ auth_time: past_timecode
211
371
  }
372
+
212
373
  token = make_hs256_token(payload)
213
374
  expect do
214
- jwt_validator.decode(token)
215
- end.to raise_error(JWT::InvalidAudError)
375
+ jwt_validator.verify(token, { max_age: 60 })
376
+ end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
377
+ 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(past_timecode + 60 + 60)})"
378
+ }))
216
379
  end
217
380
 
218
- it 'should decode a valid HS256 token with multiple audiences' do
381
+ it 'should verify a valid HS256 token with multiple audiences' do
382
+ audience = [
383
+ client_id,
384
+ "https://#{domain}/userinfo"
385
+ ]
219
386
  payload = {
220
387
  iss: "https://#{domain}/",
221
- aud: [
222
- client_id,
223
- "https://#{domain}/userinfo"
224
- ]
388
+ sub: 'sub',
389
+ aud: audience,
390
+ exp: future_timecode,
391
+ iat: past_timecode,
392
+ azp: client_id
225
393
  }
226
394
  token = make_hs256_token(payload)
227
- expect(jwt_validator.decode(token).length).to eq(2)
395
+ id_token = jwt_validator.verify(token)
396
+ expect(id_token['aud']).to eq(audience)
228
397
  end
229
398
 
230
- it 'should decode a standard HS256 token' do
399
+ it 'should verify a standard HS256 token' do
231
400
  sub = 'abc123'
232
401
  payload = {
402
+ iss: "https://#{domain}/",
233
403
  sub: sub,
404
+ aud: client_id,
234
405
  exp: future_timecode,
235
- iss: "https://#{domain}/",
236
- iat: past_timecode,
237
- aud: client_id
406
+ iat: past_timecode
238
407
  }
239
408
  token = make_hs256_token(payload)
240
- decoded_token = jwt_validator.decode(token)
241
- expect(decoded_token.first['sub']).to eq(sub)
409
+ verified_token = jwt_validator.verify(token)
410
+ expect(verified_token['sub']).to eq(sub)
242
411
  end
243
412
 
244
- it 'should decode a standard RS256 token' do
413
+ it 'should verify a standard RS256 token' do
245
414
  domain = 'example.org'
246
415
  sub = 'abc123'
247
416
  payload = {
@@ -253,8 +422,8 @@ describe OmniAuth::Auth0::JWTValidator do
253
422
  kid: jwks_kid
254
423
  }
255
424
  token = make_rs256_token(payload)
256
- decoded_token = make_jwt_validator(opt_domain: domain).decode(token)
257
- expect(decoded_token.first['sub']).to eq(sub)
425
+ verified_token = make_jwt_validator(opt_domain: domain).verify(token)
426
+ expect(verified_token['sub']).to eq(sub)
258
427
  end
259
428
  end
260
429
 
@@ -82,6 +82,8 @@ describe OmniAuth::Strategies::Auth0 do
82
82
  expect(redirect_url).to have_query('client_id')
83
83
  expect(redirect_url).to have_query('redirect_uri')
84
84
  expect(redirect_url).not_to have_query('auth0Client')
85
+ expect(redirect_url).not_to have_query('connection')
86
+ expect(redirect_url).not_to have_query('prompt')
85
87
  end
86
88
 
87
89
  it 'redirects to hosted login page' do
@@ -95,6 +97,21 @@ describe OmniAuth::Strategies::Auth0 do
95
97
  expect(redirect_url).to have_query('redirect_uri')
96
98
  expect(redirect_url).to have_query('connection', 'abcd')
97
99
  expect(redirect_url).not_to have_query('auth0Client')
100
+ expect(redirect_url).not_to have_query('prompt')
101
+ end
102
+
103
+ it 'redirects to hosted login page with prompt=login' do
104
+ get 'auth/auth0?prompt=login'
105
+ expect(last_response.status).to eq(302)
106
+ redirect_url = last_response.headers['Location']
107
+ expect(redirect_url).to start_with('https://samples.auth0.com/authorize')
108
+ expect(redirect_url).to have_query('response_type', 'code')
109
+ expect(redirect_url).to have_query('state')
110
+ expect(redirect_url).to have_query('client_id')
111
+ expect(redirect_url).to have_query('redirect_uri')
112
+ expect(redirect_url).to have_query('prompt', 'login')
113
+ expect(redirect_url).not_to have_query('auth0Client')
114
+ expect(redirect_url).not_to have_query('connection')
98
115
  end
99
116
 
100
117
  describe 'callback' do
@@ -300,7 +317,7 @@ RSpec::Matchers.define :have_query do |key, value|
300
317
  uri = redirect_uri(actual)
301
318
  query = query(uri)
302
319
  if value.nil?
303
- query[key].length == 1
320
+ query.key?(key)
304
321
  else
305
322
  query[key] == [value]
306
323
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-auth0
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Auth0
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-18 00:00:00.000000000 Z
11
+ date: 2020-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth-oauth2
@@ -52,11 +52,14 @@ extra_rdoc_files: []
52
52
  files:
53
53
  - ".circleci/config.yml"
54
54
  - ".gemrelease"
55
+ - ".github/CODEOWNERS"
55
56
  - ".github/ISSUE_TEMPLATE.md"
56
57
  - ".github/PULL_REQUEST_TEMPLATE.md"
58
+ - ".github/stale.yml"
57
59
  - ".gitignore"
58
60
  - ".rspec"
59
61
  - ".rubocop.yml"
62
+ - ".snyk"
60
63
  - CHANGELOG.md
61
64
  - CODE_OF_CONDUCT.md
62
65
  - CONTRIBUTING.md
@@ -71,6 +74,7 @@ files:
71
74
  - examples/sinatra/config.ru
72
75
  - lib/omniauth-auth0.rb
73
76
  - lib/omniauth-auth0/version.rb
77
+ - lib/omniauth/auth0/errors.rb
74
78
  - lib/omniauth/auth0/jwt_validator.rb
75
79
  - lib/omniauth/auth0/telemetry.rb
76
80
  - lib/omniauth/strategies/auth0.rb
@@ -99,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
103
  - !ruby/object:Gem::Version
100
104
  version: '0'
101
105
  requirements: []
102
- rubygems_version: 3.0.3
106
+ rubygems_version: 3.0.1
103
107
  signing_key:
104
108
  specification_version: 4
105
109
  summary: OmniAuth OAuth2 strategy for the Auth0 platform.