omniauth-auth0 2.2.0 → 2.3.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 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.