auth0 4.11.0 → 4.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5510689e9c12f2e8b6cdaa0b7a36487b426e65d550a71b8e507d1c1a6328032b
4
- data.tar.gz: 23a2e96a3dadbe065252666ec16cee463d4511ec464be83abcc05ba43f1025a7
3
+ metadata.gz: 5aa06f59c1696dbadf9345ff1843f247ca305fd5340d84b75252fe5bd465d27a
4
+ data.tar.gz: dd3d9c156870dcf9b50a48870aab8a6b44c67b560835de6b52222f217718a5b1
5
5
  SHA512:
6
- metadata.gz: f018c7269551d67247bd49841a76e379efdfad2b650efa5506bb8aa75d28576fdf66e7bd2d1f405fc1d92471d53f5ade81b86101a91ffb29053d9313f8ab9591
7
- data.tar.gz: 19a41e79a0b6bd393eb8b85c882dcff8ff7ccefa71c33896928cf53cb5068b495e3d42aa0751ad6fc4d2b103b76049f0570147accfa7b406ff329e53100d009d
6
+ metadata.gz: 8369176c7e8b3dcf740aa144650d345c4c5803e8718764d278591e13b72327c0cb6bae1b2dde710b48fe0c31991433d0433cbac9a422cbea58359abb0b986764
7
+ data.tar.gz: 5ff4c3af66bc7cd480808c98158f1a039eb765eed10ffa19d8a1c5c7bf4c9c8bdc20d8b535dd7ea5efcf934b64e8ffbd3395480b1925b33a8a6aba7550afdaf7
@@ -1,5 +1,18 @@
1
1
  # Change Log
2
2
 
3
+ ## [v4.12.0](https://github.com/auth0/ruby-auth0/tree/v4.12.0) (2020-06-10)
4
+
5
+ [Full Changelog](https://github.com/auth0/ruby-auth0/compare/v4.11.0...v4.12.0)
6
+
7
+ **Added**
8
+
9
+ - Improve OIDC compliance [SDK-987] [\#225](https://github.com/auth0/ruby-auth0/pull/225) ([Widcket](https://github.com/Widcket))
10
+
11
+ **Security**
12
+
13
+ - Bump activesupport from 6.0.3 to 6.0.3.1 [\#221](https://github.com/auth0/ruby-auth0/pull/221) ([dependabot[bot]](https://github.com/apps/dependabot))
14
+ - Bump actionpack from 6.0.3 to 6.0.3.1 [\#220](https://github.com/auth0/ruby-auth0/pull/220) ([dependabot[bot]](https://github.com/apps/dependabot))
15
+
3
16
  ## [v4.11.0](https://github.com/auth0/ruby-auth0/tree/v4.11.0) (2020-05-06)
4
17
 
5
18
  [Full Changelog](https://github.com/auth0/ruby-auth0/compare/v4.10.0...v4.11.0)
@@ -1,26 +1,28 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- auth0 (4.11.0)
4
+ auth0 (4.12.0)
5
+ jwt (~> 2.2.0)
5
6
  rest-client (~> 2.0.0)
7
+ zache (~> 0.12.0)
6
8
 
7
9
  GEM
8
10
  remote: https://rubygems.org/
9
11
  specs:
10
- actionpack (6.0.3)
11
- actionview (= 6.0.3)
12
- activesupport (= 6.0.3)
12
+ actionpack (6.0.3.1)
13
+ actionview (= 6.0.3.1)
14
+ activesupport (= 6.0.3.1)
13
15
  rack (~> 2.0, >= 2.0.8)
14
16
  rack-test (>= 0.6.3)
15
17
  rails-dom-testing (~> 2.0)
16
18
  rails-html-sanitizer (~> 1.0, >= 1.2.0)
17
- actionview (6.0.3)
18
- activesupport (= 6.0.3)
19
+ actionview (6.0.3.1)
20
+ activesupport (= 6.0.3.1)
19
21
  builder (~> 3.1)
20
22
  erubi (~> 1.4)
21
23
  rails-dom-testing (~> 2.0)
22
24
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
23
- activesupport (6.0.3)
25
+ activesupport (6.0.3.1)
24
26
  concurrent-ruby (~> 1.0, >= 1.0.2)
25
27
  i18n (>= 0.7, < 2)
26
28
  minitest (~> 5.1)
@@ -28,13 +30,13 @@ GEM
28
30
  zeitwerk (~> 2.2, >= 2.2.2)
29
31
  addressable (2.7.0)
30
32
  public_suffix (>= 2.0.2, < 5.0)
31
- ast (2.4.0)
33
+ ast (2.4.1)
32
34
  builder (3.2.4)
33
- codecov (0.1.16)
35
+ codecov (0.1.17)
34
36
  json
35
37
  simplecov
36
38
  url
37
- coderay (1.1.2)
39
+ coderay (1.1.3)
38
40
  concurrent-ruby (1.1.6)
39
41
  coveralls (0.7.1)
40
42
  multi_json (~> 1.3)
@@ -56,7 +58,7 @@ GEM
56
58
  erubi (1.9.0)
57
59
  faker (1.9.6)
58
60
  i18n (>= 0.7)
59
- ffi (1.12.2)
61
+ ffi (1.13.1)
60
62
  formatador (0.2.5)
61
63
  fuubar (2.5.0)
62
64
  rspec-core (~> 3.0)
@@ -79,23 +81,23 @@ GEM
79
81
  hashdiff (1.0.1)
80
82
  http-cookie (1.0.3)
81
83
  domain_name (~> 0.5)
82
- i18n (1.8.2)
84
+ i18n (1.8.3)
83
85
  concurrent-ruby (~> 1.0)
84
- jaro_winkler (1.5.4)
85
86
  json (2.3.0)
87
+ jwt (2.2.1)
86
88
  listen (3.2.1)
87
89
  rb-fsevent (~> 0.10, >= 0.10.3)
88
90
  rb-inotify (~> 0.9, >= 0.9.10)
89
91
  loofah (2.5.0)
90
92
  crass (~> 1.0.2)
91
93
  nokogiri (>= 1.5.9)
92
- lumberjack (1.2.4)
94
+ lumberjack (1.2.5)
93
95
  method_source (0.8.2)
94
96
  mime-types (3.3.1)
95
97
  mime-types-data (~> 3.2015)
96
- mime-types-data (3.2020.0425)
98
+ mime-types-data (3.2020.0512)
97
99
  mini_portile2 (2.4.0)
98
- minitest (5.14.0)
100
+ minitest (5.14.1)
99
101
  multi_json (1.14.1)
100
102
  nenv (0.3.0)
101
103
  netrc (0.11.0)
@@ -105,7 +107,7 @@ GEM
105
107
  nenv (~> 0.1)
106
108
  shellany (~> 0.0)
107
109
  parallel (1.19.1)
108
- parser (2.7.1.2)
110
+ parser (2.7.1.3)
109
111
  ast (~> 2.4.0)
110
112
  pry (0.10.4)
111
113
  coderay (~> 1.1.0)
@@ -113,8 +115,8 @@ GEM
113
115
  slop (~> 3.4)
114
116
  pry-nav (0.2.4)
115
117
  pry (>= 0.9.10, < 0.11.0)
116
- public_suffix (4.0.4)
117
- rack (2.1.2)
118
+ public_suffix (4.0.5)
119
+ rack (2.1.3)
118
120
  rack-test (0.8.3)
119
121
  rack (>= 1.0, < 3)
120
122
  rails-dom-testing (2.0.3)
@@ -122,9 +124,9 @@ GEM
122
124
  nokogiri (>= 1.6)
123
125
  rails-html-sanitizer (1.3.0)
124
126
  loofah (~> 2.3)
125
- railties (6.0.3)
126
- actionpack (= 6.0.3)
127
- activesupport (= 6.0.3)
127
+ railties (6.0.3.1)
128
+ actionpack (= 6.0.3.1)
129
+ activesupport (= 6.0.3.1)
128
130
  method_source
129
131
  rake (>= 0.8.7)
130
132
  thor (>= 0.20.3, < 2.0)
@@ -133,6 +135,7 @@ GEM
133
135
  rb-fsevent (0.10.4)
134
136
  rb-inotify (0.10.1)
135
137
  ffi (~> 1.0)
138
+ regexp_parser (1.7.1)
136
139
  rest-client (2.0.2)
137
140
  http-cookie (>= 1.0.2, < 2.0)
138
141
  mime-types (>= 1.16, < 4.0)
@@ -144,25 +147,28 @@ GEM
144
147
  rspec-mocks (~> 3.9.0)
145
148
  rspec-core (3.9.2)
146
149
  rspec-support (~> 3.9.3)
147
- rspec-expectations (3.9.1)
150
+ rspec-expectations (3.9.2)
148
151
  diff-lcs (>= 1.2.0, < 2.0)
149
152
  rspec-support (~> 3.9.0)
150
153
  rspec-mocks (3.9.1)
151
154
  diff-lcs (>= 1.2.0, < 2.0)
152
155
  rspec-support (~> 3.9.0)
153
156
  rspec-support (3.9.3)
154
- rubocop (0.82.0)
155
- jaro_winkler (~> 1.5.1)
157
+ rubocop (0.85.1)
156
158
  parallel (~> 1.10)
157
159
  parser (>= 2.7.0.1)
158
160
  rainbow (>= 2.2.2, < 4.0)
161
+ regexp_parser (>= 1.7)
159
162
  rexml
163
+ rubocop-ast (>= 0.0.3)
160
164
  ruby-progressbar (~> 1.7)
161
165
  unicode-display_width (>= 1.4.0, < 2.0)
162
- rubocop-rails (2.5.2)
163
- activesupport
166
+ rubocop-ast (0.0.3)
167
+ parser (>= 2.7.0.1)
168
+ rubocop-rails (2.6.0)
169
+ activesupport (>= 4.2.0)
164
170
  rack (>= 1.1)
165
- rubocop (>= 0.72.0)
171
+ rubocop (>= 0.82.0)
166
172
  ruby-progressbar (1.10.1)
167
173
  safe_yaml (1.0.5)
168
174
  shellany (0.0.1)
@@ -177,7 +183,7 @@ GEM
177
183
  terminal-notifier-guard (1.7.0)
178
184
  thor (1.0.1)
179
185
  thread_safe (0.3.6)
180
- tins (1.24.1)
186
+ tins (1.25.0)
181
187
  sync
182
188
  tzinfo (1.2.7)
183
189
  thread_safe (~> 0.1)
@@ -186,12 +192,13 @@ GEM
186
192
  unf_ext (0.0.7.7)
187
193
  unicode-display_width (1.7.0)
188
194
  url (0.3.2)
189
- vcr (5.1.0)
195
+ vcr (6.0.0)
190
196
  webmock (3.8.3)
191
197
  addressable (>= 2.3.6)
192
198
  crack (>= 0.3.2)
193
199
  hashdiff (>= 0.4.0, < 2.0.0)
194
200
  yard (0.9.25)
201
+ zache (0.12.0)
195
202
  zeitwerk (2.3.0)
196
203
 
197
204
  PLATFORMS
data/README.md CHANGED
@@ -118,6 +118,51 @@ In addition to the Management API, this SDK also provides access to [Authenticat
118
118
 
119
119
  Please note that this module implements endpoints that might be deprecated for newer tenants. If you have any questions about how and when the endpoints should be used, consult the [documentation](https://auth0.com/docs/api/authentication) or ask in our [Community forums](https://community.auth0.com/tags/wordpress).
120
120
 
121
+ ## ID Token Validation
122
+
123
+ An ID token may be present in the credentials received after authentication. This token contains information associated with the user that has just logged in, provided the scope used contained `openid`. You can [read more about ID tokens here](https://auth0.com/docs/tokens/concepts/id-tokens).
124
+
125
+ Before accessing its contents, you must first validate the ID token to ensure it has not been tampered with and that it is meant for your application to consume. Use the `validate_id_token` method to do so:
126
+
127
+ ```ruby
128
+ begin
129
+ @auth0_client.validate_id_token 'YOUR_ID_TOKEN'
130
+ rescue Auth0::InvalidIdToken => e
131
+ # In this case the ID Token contents should not be trusted
132
+ end
133
+ ```
134
+
135
+ The method takes the following optional keyword parameters:
136
+
137
+ | Parameter | Type | Description | Default value |
138
+ | ------------- | -------------- | ------------- | ------------------------- |
139
+ | `algorithm` | `JWTAlgorithm` | The [signing algorithm](https://auth0.com/docs/tokens/concepts/signing-algorithms) used by your Auth0 application. | `Auth0::Algorithm::RS256` (using the [JWKS URL](https://auth0.com/docs/tokens/concepts/jwks) of your **Auth0 Domain**) |
140
+ | `leeway` | Integer | Number of seconds to account for clock skew when validating the `exp`, `iat` and `azp` claims. | `60` |
141
+ | `nonce` | String | The `nonce` value you sent in the call to `/authorize`, if any. | `nil` |
142
+ | `max_age` | Integer | The `max_age` value you sent in the call to `/authorize`, if any. | `nil` |
143
+ | `issuer` | String | By default the `iss` claim will be checked against the URL of your **Auth0 Domain**. Use this parameter to override that. | `nil` |
144
+ | `audience` | String | By default the `aud` claim will be compared to your **Auth0 Client ID**. Use this parameter to override that. | `nil` |
145
+
146
+ You can check the signing algorithm value under **Advanced Settings > OAuth > JsonWebToken Signature Algorithm** in your Auth0 application settings panel. [We recommend](https://auth0.com/docs/tokens/concepts/signing-algorithms#our-recommendation) that you make use of asymmetric signing algorithms like `RS256` instead of symmetric ones like `HS256`.
147
+
148
+ ```ruby
149
+ # HS256
150
+
151
+ begin
152
+ @auth0_client.validate_id_token 'YOUR_ID_TOKEN', algorithm: Auth0::Algorithm::HS256.secret('YOUR_SECRET')
153
+ rescue Auth0::InvalidIdToken => e
154
+ # Handle error
155
+ end
156
+
157
+ # RS256 with a custom JWKS URL
158
+
159
+ begin
160
+ @auth0_client.validate_id_token 'YOUR_ID_TOKEN', algorithm: Auth0::Algorithm::RS256.jwks_url('YOUR_URL')
161
+ rescue Auth0::InvalidIdToken => e
162
+ # Handle error
163
+ end
164
+ ```
165
+
121
166
  ## Development
122
167
 
123
168
  In order to set up the local environment you'd have to have Ruby installed and a few global gems used to run and record the unit tests. A working Ruby version can be taken from the [CI script](/.circleci/config.yml). At the moment of this writting we're using Ruby `2.5.7`.
@@ -17,6 +17,8 @@ Gem::Specification.new do |s|
17
17
  s.require_paths = ['lib']
18
18
 
19
19
  s.add_runtime_dependency 'rest-client', '~> 2.0.0'
20
+ s.add_runtime_dependency 'jwt', '~> 2.2.0'
21
+ s.add_runtime_dependency 'zache', '~> 0.12.0'
20
22
 
21
23
  s.add_development_dependency 'rake', '~> 13.0'
22
24
  s.add_development_dependency 'fuubar', '~> 2.0'
@@ -1,6 +1,7 @@
1
1
  require 'auth0/version'
2
2
  require 'auth0/mixins'
3
3
  require 'auth0/exception'
4
+ require 'auth0/algorithm'
4
5
  require 'auth0/client'
5
6
  require 'auth0_client'
6
7
  # Namespace for ruby-auth0 logic
@@ -0,0 +1,5 @@
1
+ module Auth0
2
+ module Algorithm
3
+ include Auth0::Mixins::Validation::Algorithm
4
+ end
5
+ end
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  # rubocop:disable Metrics/ModuleLength
3
+
4
+ require 'jwt'
5
+
2
6
  module Auth0
3
7
  module Api
4
8
  # {https://auth0.com/docs/api/authentication}
@@ -502,6 +506,36 @@ module Auth0
502
506
  post('/unlink', request_params)
503
507
  end
504
508
 
509
+ # Validate an ID token (signature and expiration).
510
+ # @see https://auth0.com/docs/tokens/guides/validate-id-tokens
511
+ # @param id_token [string] The JWT to validate.
512
+ # @param algorithm [JWKAlgorithm] The expected signing algorithm.
513
+ # Defaults to +Auth0::Algorithm::RS256.jwks_url("https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json", lifetime: 10 * 60)+.
514
+ # @param leeway [integer] The clock skew to accept when verifying date related claims in seconds.
515
+ # Must be a non-negative value. Defaults to *60 seconds*.
516
+ # @param nonce [string] The nonce value sent during authentication.
517
+ # @param max_age [integer] The max_age value sent during authentication.
518
+ # Must be a non-negative value.
519
+ # @param issuer [string] The expected issuer claim value.
520
+ # Defaults to +https://YOUR_AUTH0_DOMAIN/+.
521
+ # @param audience [string] The expected audience claim value.
522
+ # Defaults to your *Auth0 Client ID*.
523
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/ParameterLists
524
+ def validate_id_token(id_token, algorithm: nil, leeway: 60, nonce: nil, max_age: nil, issuer: nil, audience: nil)
525
+ context = {
526
+ issuer: issuer || "https://#{@domain}/",
527
+ audience: audience || @client_id,
528
+ algorithm: algorithm || Auth0::Algorithm::RS256.jwks_url("https://#{@domain}/.well-known/jwks.json"),
529
+ leeway: leeway
530
+ }
531
+
532
+ context[:nonce] = nonce unless nonce.nil?
533
+ context[:max_age] = max_age unless max_age.nil?
534
+
535
+ Auth0::Mixins::Validation::IdTokenValidator.new(context).validate(id_token)
536
+ end
537
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/ParameterLists
538
+
505
539
  private
506
540
 
507
541
  # Build a URL query string from a hash.
@@ -59,4 +59,6 @@ module Auth0
59
59
  Time.at(headers['X-RateLimit-Reset']).utc
60
60
  end
61
61
  end
62
+
63
+ class InvalidIdToken < Auth0::Exception; end
62
64
  end
@@ -1,3 +1,11 @@
1
+ require 'zache'
2
+
3
+ class Zache
4
+ def last(key)
5
+ @hash[key][:value] if @hash.key?(key)
6
+ end
7
+ end
8
+
1
9
  module Auth0
2
10
  module Mixins
3
11
  # Module to provide validation for specific data structures.
@@ -20,6 +28,313 @@ module Auth0
20
28
  permissions.map { |permission| permission.to_h }
21
29
  end
22
30
 
31
+ # rubocop:disable Metrics/ClassLength
32
+ class IdTokenValidator
33
+ def initialize(context)
34
+ @context = context
35
+ end
36
+
37
+ def validate(id_token)
38
+ decoding_error = 'ID token could not be decoded'
39
+
40
+ unless !id_token.to_s.empty? && id_token.split('.').count == 3
41
+ raise Auth0::InvalidIdToken, decoding_error
42
+ end
43
+
44
+ begin
45
+ header = JWT::JSON.parse(JWT::Base64.url_decode(id_token.split('.').first))
46
+ rescue
47
+ raise Auth0::InvalidIdToken, decoding_error
48
+ end
49
+
50
+ claims = decode_and_validate_signature(id_token, header)
51
+ validate_claims(claims)
52
+ end
53
+
54
+ private
55
+
56
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
57
+ def decode_and_validate_signature(id_token, header)
58
+ algorithm = @context[:algorithm]
59
+
60
+ unless algorithm.is_a?(Auth0::Mixins::Validation::JWTAlgorithm)
61
+ raise Auth0::InvalidIdToken, "Signature algorithm of \"#{algorithm}\" is not supported"
62
+ end
63
+
64
+ # The expiration verification will be performed in the validate_claims method
65
+ options = { algorithms: [algorithm.name], verify_expiration: false, verify_not_before: false }
66
+ secret = nil
67
+
68
+ case algorithm
69
+ when Auth0::Algorithm::RS256
70
+ kid = header['kid']
71
+ jwks = JSON.parse(JSON[algorithm.jwks], symbolize_names: true)
72
+
73
+ if !jwks[:keys].find { |key| key[:kid] == kid } && !algorithm.fetched_jwks?
74
+ jwks = JSON.parse(JSON[algorithm.jwks(force: true)], symbolize_names: true)
75
+ end
76
+
77
+ options[:jwks] = jwks
78
+ when Auth0::Algorithm::HS256
79
+ secret = algorithm.secret
80
+ end
81
+
82
+ begin
83
+ result = JWT.decode(id_token, secret, true, options)
84
+ result.first
85
+ rescue JWT::VerificationError
86
+ raise Auth0::InvalidIdToken, 'Invalid ID token signature'
87
+ rescue JWT::IncorrectAlgorithm
88
+ alg = header['alg']
89
+ raise Auth0::InvalidIdToken, "Signature algorithm of \"#{alg}\" is not supported. Expected the ID token"\
90
+ " to be signed with \"#{algorithm.name}\""
91
+ rescue JWT::DecodeError
92
+ raise Auth0::InvalidIdToken, "Could not find a public key for Key ID (kid) \"#{kid}\""
93
+ end
94
+ end
95
+
96
+ # rubocop:disable Metrics/PerceivedComplexity
97
+ def validate_claims(claims)
98
+ leeway = @context[:leeway]
99
+ nonce = @context[:nonce]
100
+ issuer = @context[:issuer]
101
+ audience = @context[:audience]
102
+ max_age = @context[:max_age]
103
+
104
+ raise Auth0::InvalidParameter, 'Must supply a valid leeway' unless leeway.is_a?(Integer) && leeway >= 0
105
+ raise Auth0::InvalidParameter, 'Must supply a valid nonce' unless nonce.nil? || !nonce.to_s.empty?
106
+ raise Auth0::InvalidParameter, 'Must supply a valid issuer' unless issuer.nil? || !issuer.to_s.empty?
107
+ raise Auth0::InvalidParameter, 'Must supply a valid audience' unless audience.nil? || !audience.to_s.empty?
108
+
109
+ unless max_age.nil? || (max_age.is_a?(Integer) && max_age >= 0)
110
+ raise Auth0::InvalidParameter, 'Must supply a valid max_age'
111
+ end
112
+
113
+ validate_iss(claims, issuer)
114
+ validate_sub(claims)
115
+ validate_aud(claims, audience)
116
+ validate_exp(claims, leeway)
117
+ validate_iat(claims, leeway)
118
+ validate_nonce(claims, nonce) if nonce
119
+ validate_azp(claims, audience) if claims['aud'].is_a?(Array) && claims['aud'].count > 1
120
+ validate_auth_time(claims, max_age, leeway) if max_age
121
+ end
122
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
123
+
124
+ def validate_iss(claims, expected)
125
+ unless claims.key?('iss') && claims['iss'].is_a?(String)
126
+ raise Auth0::InvalidIdToken, 'Issuer (iss) claim must be a string present in the ID token'
127
+ end
128
+
129
+ unless expected == claims['iss']
130
+ raise Auth0::InvalidIdToken, "Issuer (iss) claim mismatch in the ID token; expected \"#{expected}\","\
131
+ " found \"#{claims['iss']}\""
132
+ end
133
+ end
134
+
135
+ def validate_sub(claims)
136
+ unless claims.key?('sub') && claims['sub'].is_a?(String)
137
+ raise Auth0::InvalidIdToken, 'Subject (sub) claim must be a string present in the ID token'
138
+ end
139
+ end
140
+
141
+ def validate_aud(claims, expected)
142
+ unless claims.key?('aud') && (claims['aud'].is_a?(String) || claims['aud'].is_a?(Array))
143
+ raise Auth0::InvalidIdToken, 'Audience (aud) claim must be a string or array of strings present'\
144
+ ' in the ID token'
145
+ end
146
+
147
+ if claims['aud'].is_a?(String) && expected != claims['aud']
148
+ raise Auth0::InvalidIdToken, "Audience (aud) claim mismatch in the ID token; expected \"#{expected}\","\
149
+ " found \"#{claims['aud']}\""
150
+ elsif claims['aud'].is_a?(Array) && !claims['aud'].include?(expected)
151
+ raise Auth0::InvalidIdToken, "Audience (aud) claim mismatch in the ID token; expected \"#{expected}\""\
152
+ " but was not one of \"#{claims['aud'].join ', '}\""
153
+ end
154
+ end
155
+
156
+ def validate_exp(claims, leeway)
157
+ unless claims.key?('exp') && claims['exp'].is_a?(Integer)
158
+ raise Auth0::InvalidIdToken, 'Expiration Time (exp) claim must be a number present in the ID token'
159
+ end
160
+
161
+ now = @context[:clock] || Time.now.to_i
162
+ exp_time = claims['exp'] + leeway
163
+
164
+ unless now < exp_time
165
+ raise Auth0::InvalidIdToken, 'Expiration Time (exp) claim mismatch in the ID token; current time'\
166
+ " \"#{now}\" is after expiration time \"#{exp_time}\""
167
+ end
168
+ end
169
+
170
+ def validate_iat(claims, leeway)
171
+ unless claims.key?('iat') && claims['iat'].is_a?(Integer)
172
+ raise Auth0::InvalidIdToken, 'Issued At (iat) claim must be a number present in the ID token'
173
+ end
174
+
175
+ now = @context[:clock] || Time.now.to_i
176
+ iat_time = claims['iat'] - leeway
177
+
178
+ unless now > iat_time
179
+ raise Auth0::InvalidIdToken, "Issued At (iat) claim mismatch in the ID token; current time \"#{now}\""\
180
+ " is before issued at time \"#{iat_time}\""
181
+ end
182
+ end
183
+
184
+ def validate_nonce(claims, expected)
185
+ unless claims.key?('nonce') && claims['nonce'].is_a?(String)
186
+ raise Auth0::InvalidIdToken, 'Nonce (nonce) claim must be a string present in the ID token'
187
+ end
188
+
189
+ unless expected == claims['nonce']
190
+ raise Auth0::InvalidIdToken, "Nonce (nonce) claim mismatch in the ID token; expected \"#{expected}\","\
191
+ " found \"#{claims['nonce']}\""
192
+ end
193
+ end
194
+
195
+ def validate_azp(claims, expected)
196
+ unless claims.key?('azp') && claims['azp'].is_a?(String)
197
+ raise Auth0::InvalidIdToken, 'Authorized Party (azp) claim must be a string present in the ID token'
198
+ end
199
+
200
+ unless expected == claims['azp']
201
+ raise Auth0::InvalidIdToken, 'Authorized Party (azp) claim mismatch in the ID token; expected'\
202
+ " \"#{expected}\", found \"#{claims['azp']}\""
203
+ end
204
+ end
205
+
206
+ def validate_auth_time(claims, max_age, leeway)
207
+ unless claims.key?('auth_time') && claims['auth_time'].is_a?(Integer)
208
+ raise Auth0::InvalidIdToken, 'Authentication Time (auth_time) claim must be a number present in the ID'\
209
+ ' token when Max Age (max_age) is specified'
210
+ end
211
+
212
+ now = @context[:clock] || Time.now.to_i
213
+ auth_valid_until = claims['auth_time'] + max_age + leeway
214
+
215
+ unless now < auth_valid_until
216
+ raise Auth0::InvalidIdToken, 'Authentication Time (auth_time) claim in the ID token indicates that too'\
217
+ ' much time has passed since the last end-user authentication. Current time'\
218
+ " \"#{now}\" is after last auth at \"#{auth_valid_until}\""
219
+ end
220
+ end
221
+ end
222
+ # rubocop:enable Metrics/ClassLength
223
+
224
+ class JWTAlgorithm
225
+ private_class_method :new
226
+
227
+ def name
228
+ raise RuntimeError, 'Must be overriden by the subclasses'
229
+ end
230
+ end
231
+
232
+ module Algorithm
233
+ # Represents the HS256 algorithm, which rely on shared secrets.
234
+ # @see https://auth0.com/docs/tokens/concepts/signing-algorithms
235
+ class HS256 < JWTAlgorithm
236
+ class << self
237
+ private :new
238
+
239
+ # Create a new instance passing the shared secret.
240
+ # @param secret [string] The HMAC shared secret.
241
+ # @return [HS256] A new instance.
242
+ def secret(secret)
243
+ new secret
244
+ end
245
+ end
246
+
247
+ attr_accessor :secret
248
+
249
+ def initialize(secret)
250
+ raise Auth0::InvalidParameter, 'Must supply a valid secret' if secret.to_s.empty?
251
+
252
+ @secret = secret
253
+ end
254
+
255
+ # Returns the algorithm name.
256
+ # @return [string] The algorithm name.
257
+ def name
258
+ 'HS256'
259
+ end
260
+ end
261
+
262
+ # Represents the RS256 algorithm, which rely on public key certificates.
263
+ # @see https://auth0.com/docs/tokens/concepts/signing-algorithms
264
+ class RS256 < JWTAlgorithm
265
+ include Auth0::Mixins::HTTPProxy
266
+
267
+ @@cache = Zache.new.freeze
268
+
269
+ class << self
270
+ private :new
271
+
272
+ # Create a new instance passing the JWK set url.
273
+ # @param url [string] The url where the JWK set is located.
274
+ # @param lifetime [integer] The lifetime of the JWK set in-memory cache in seconds.
275
+ # Must be a non-negative value. Defaults to *600 seconds* (10 minutes).
276
+ # @return [RS256] A new instance.
277
+ def jwks_url(url, lifetime: 10 * 60)
278
+ new url, lifetime
279
+ end
280
+
281
+ # Clear the JWK set cache.
282
+ def remove_jwks
283
+ @@cache.remove(:jwks)
284
+ end
285
+ end
286
+
287
+ def initialize(jwks_url, lifetime)
288
+ raise Auth0::InvalidParameter, 'Must supply a valid jwks_url' if jwks_url.to_s.empty?
289
+ raise Auth0::InvalidParameter, 'Must supply a valid lifetime' unless lifetime.is_a?(Integer) && lifetime >= 0
290
+
291
+ @lifetime = lifetime
292
+ @jwks_url = jwks_url
293
+ @did_fetch_jwks = false
294
+ end
295
+
296
+ # Returns the algorithm name.
297
+ # @return [string] The algorithm name.
298
+ def name
299
+ 'RS256'
300
+ end
301
+
302
+ # Fetches the JWK set from the in-memory cache or from the url.
303
+ # @return [hash] A JWK set.
304
+ def jwks(force: false)
305
+ result = fetch_jwks if force
306
+
307
+ if result
308
+ @@cache.put(:jwks, result, lifetime: @lifetime)
309
+ return result
310
+ end
311
+
312
+ previous_value = @@cache.last(:jwks)
313
+
314
+ @@cache.get(:jwks, lifetime: @lifetime, dirty: true) do
315
+ new_value = fetch_jwks
316
+
317
+ raise Auth0::InvalidIdToken, 'Could not fetch the JWK set' unless new_value || previous_value
318
+
319
+ new_value || previous_value
320
+ end
321
+ end
322
+
323
+ # Returns whether or not the JWK set was fetched from the url.
324
+ # @return [boolean] +true+ if a request to the JWK set url was made, +false+ otherwise.
325
+ def fetched_jwks?
326
+ @did_fetch_jwks
327
+ end
328
+
329
+ private
330
+
331
+ def fetch_jwks
332
+ result = get(@jwks_url)
333
+ @did_fetch_jwks = result.is_a?(Hash) && result.key?('keys')
334
+ result if @did_fetch_jwks
335
+ end
336
+ end
337
+ end
23
338
  end
24
339
  end
25
340
  end