auth0 4.10.0 → 4.15.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.
@@ -6,6 +6,7 @@ require 'auth0/api/v2/connections'
6
6
  require 'auth0/api/v2/device_credentials'
7
7
  require 'auth0/api/v2/emails'
8
8
  require 'auth0/api/v2/jobs'
9
+ require 'auth0/api/v2/prompts'
9
10
  require 'auth0/api/v2/rules'
10
11
  require 'auth0/api/v2/roles'
11
12
  require 'auth0/api/v2/stats'
@@ -15,6 +16,7 @@ require 'auth0/api/v2/user_blocks'
15
16
  require 'auth0/api/v2/tenants'
16
17
  require 'auth0/api/v2/tickets'
17
18
  require 'auth0/api/v2/logs'
19
+ require 'auth0/api/v2/log_streams'
18
20
  require 'auth0/api/v2/resource_servers'
19
21
  require 'auth0/api/v2/guardian'
20
22
 
@@ -30,6 +32,7 @@ module Auth0
30
32
  include Auth0::Api::V2::DeviceCredentials
31
33
  include Auth0::Api::V2::Emails
32
34
  include Auth0::Api::V2::Jobs
35
+ include Auth0::Api::V2::Prompts
33
36
  include Auth0::Api::V2::Rules
34
37
  include Auth0::Api::V2::Roles
35
38
  include Auth0::Api::V2::Stats
@@ -39,6 +42,7 @@ module Auth0
39
42
  include Auth0::Api::V2::Tenants
40
43
  include Auth0::Api::V2::Tickets
41
44
  include Auth0::Api::V2::Logs
45
+ include Auth0::Api::V2::LogStreams
42
46
  include Auth0::Api::V2::ResourceServers
43
47
  include Auth0::Api::V2::Guardian
44
48
  end
@@ -0,0 +1,78 @@
1
+ module Auth0
2
+ module Api
3
+ module V2
4
+ # Methods to use the log streams endpoints
5
+ module LogStreams
6
+ attr_reader :log_streams_path
7
+
8
+ # Retrieves a list of all log streams.
9
+ # @see https://auth0.com/docs/api/management/v2#!/Log_Streams/get_log_streams
10
+ # @return [json] Returns the log streams.
11
+ def log_streams()
12
+ get(log_streams_path)
13
+ end
14
+ alias get_log_streams log_streams
15
+
16
+ # Retrieves a log stream by its ID.
17
+ # @see https://auth0.com/docs/api/management/v2#!/Log_Streams/get_log_streams_by_id
18
+ # @param id [string] The id of the log stream to retrieve.
19
+ #
20
+ # @return [json] Returns the log stream.
21
+ def log_stream(id)
22
+ raise Auth0::InvalidParameter, 'Must supply a valid log stream id' if id.to_s.empty?
23
+ path = "#{log_streams_path}/#{id}"
24
+ get(path)
25
+ end
26
+ alias get_log_stream log_stream
27
+
28
+ # Creates a new log stream according to the JSON object received in body.
29
+ # @see https://auth0.com/docs/api/management/v2#!/Log_Streams/post_log_streams
30
+ # @param name [string] The name of the log stream.
31
+ # @param type [string] The type of log stream
32
+ # @param options [hash] The Hash options used to define the log streams's properties.
33
+ #
34
+ # @return [json] Returns the log stream.
35
+ def create_log_stream(name, type, options)
36
+ raise Auth0::InvalidParameter, 'Name must contain at least one character' if name.to_s.empty?
37
+ raise Auth0::InvalidParameter, 'Must specify a valid type' if type.to_s.empty?
38
+ raise Auth0::InvalidParameter, 'Must supply a valid hash for options' unless options.is_a? Hash
39
+
40
+ request_params = {}
41
+ request_params[:name] = name
42
+ request_params[:type] = type
43
+ request_params[:sink] = options
44
+ post(log_streams_path, request_params)
45
+ end
46
+
47
+ # Deletes a log stream by its ID.
48
+ # @see https://auth0.com/docs/api/management/v2#!/Log_Streams/delete_log_streams_by_id
49
+ # @param id [string] The id of the log stream to delete.
50
+ def delete_log_stream(id)
51
+ raise Auth0::InvalidParameter, 'Must supply a valid log stream id' if id.to_s.empty?
52
+ path = "#{log_streams_path}/#{id}"
53
+ delete(path)
54
+ end
55
+
56
+ # Updates a log stream.
57
+ # @see https://auth0.com/docs/api/management/v2#!/Log_Streams/patch_log_streams_by_id
58
+ # @param id [string] The id or audience of the log stream to update.
59
+ # @param status [string] The Hash options used to define the log streams's properties.
60
+ def patch_log_stream(id, status)
61
+ raise Auth0::InvalidParameter, 'Must specify a log stream id' if id.to_s.empty?
62
+ raise Auth0::InvalidParameter, 'Must specify a valid status' if status.to_s.empty?
63
+
64
+ request_params = {}
65
+ request_params[:status] = status
66
+ path = "#{log_streams_path}/#{id}"
67
+ patch(path, request_params)
68
+ end
69
+
70
+ private
71
+ # Log Streams API path
72
+ def log_streams_path
73
+ @log_streams_path ||= '/api/v2/log-streams'
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,70 @@
1
+ module Auth0
2
+ module Api
3
+ module V2
4
+ module Prompts
5
+ attr_reader :prompts_path
6
+
7
+ # Get prompts settings.
8
+ # @see https://auth0.com/docs/api/management/v2#!/Prompts/get_prompts
9
+ # @return [json] Returns the prompts setting.
10
+ def prompts
11
+ get(prompts_path)
12
+ end
13
+ alias get_prompts prompts
14
+
15
+ # Update prompts settings.
16
+ # @see https://auth0.com/docs/api/management/v2#!/Prompts/patch_prompts
17
+ # @param options [hash]
18
+ # * :universal_login_experience [string] Should be any of: new, classic.
19
+ #
20
+ # @return [json] Returns the prompts settings.
21
+ def patch_prompts(options = {})
22
+ request_params = {
23
+ universal_login_experience: options.fetch(:universal_login_experience, nil)
24
+ }
25
+ patch(prompts_path, request_params)
26
+ end
27
+ alias update_prompts patch_prompts
28
+
29
+ # Get custom text for a prompt
30
+ # Retrieve custom text for a specific prompt and language.
31
+ # @see https://auth0.com/docs/api/management/v2#!/Prompts/get_custom_text_by_language
32
+ # @param prompt [string] Prompt of custom texts to update.
33
+ # @param language [string] Language of custom texts to update.
34
+ #
35
+ # @return [json] Returns the custom texts.
36
+ def custom_text(prompt, language)
37
+ raise Auth0::InvalidParameter, 'Must supply a valid prompt' if prompt.to_s.empty?
38
+ raise Auth0::InvalidParameter, 'Must supply a valid language' if language.to_s.empty?
39
+
40
+ path = "#{prompts_path}/#{prompt}/custom-text/#{language}"
41
+ get(path)
42
+ end
43
+ alias get_custom_text custom_text
44
+
45
+ # Set custom text for a specific prompt
46
+ # Existing texts will be overwritten.
47
+ # @see https://auth0.com/docs/api/management/v2#!/Prompts/put_custom_text_by_language
48
+ # @param prompt [string] Prompt of custom texts to update.
49
+ # @param language [string] Language of custom texts to update.
50
+ # @param body [hash] Custom texts.
51
+ #
52
+ # @return [json] Returns the custom texts.
53
+ def put_custom_text(prompt, language, body)
54
+ raise Auth0::InvalidParameter, 'Must supply a valid prompt' if prompt.to_s.empty?
55
+ raise Auth0::InvalidParameter, 'Must supply a valid language' if language.to_s.empty?
56
+
57
+ path = "#{prompts_path}/#{prompt}/custom-text/#{language}"
58
+ put(path, body)
59
+ end
60
+ alias update_custom_text put_custom_text
61
+
62
+ private
63
+
64
+ def prompts_path
65
+ @prompts_path ||= '/api/v2/prompts'
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -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
@@ -21,7 +21,7 @@ module Auth0
21
21
  elsif method == :delete
22
22
  call(:delete, url(safe_path), timeout, add_headers({params: body}))
23
23
  elsif method == :delete_with_body
24
- call(:delete, url(safe_path), timeout, headers, body)
24
+ call(:delete, url(safe_path), timeout, headers, body.to_json)
25
25
  elsif method == :post_file
26
26
  body.merge!(multipart: true)
27
27
  call(:post, url(safe_path), timeout, headers, body)
@@ -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,305 @@ 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
+ end
175
+
176
+ def validate_nonce(claims, expected)
177
+ unless claims.key?('nonce') && claims['nonce'].is_a?(String)
178
+ raise Auth0::InvalidIdToken, 'Nonce (nonce) claim must be a string present in the ID token'
179
+ end
180
+
181
+ unless expected == claims['nonce']
182
+ raise Auth0::InvalidIdToken, "Nonce (nonce) claim mismatch in the ID token; expected \"#{expected}\","\
183
+ " found \"#{claims['nonce']}\""
184
+ end
185
+ end
186
+
187
+ def validate_azp(claims, expected)
188
+ unless claims.key?('azp') && claims['azp'].is_a?(String)
189
+ raise Auth0::InvalidIdToken, 'Authorized Party (azp) claim must be a string present in the ID token'
190
+ end
191
+
192
+ unless expected == claims['azp']
193
+ raise Auth0::InvalidIdToken, 'Authorized Party (azp) claim mismatch in the ID token; expected'\
194
+ " \"#{expected}\", found \"#{claims['azp']}\""
195
+ end
196
+ end
197
+
198
+ def validate_auth_time(claims, max_age, leeway)
199
+ unless claims.key?('auth_time') && claims['auth_time'].is_a?(Integer)
200
+ raise Auth0::InvalidIdToken, 'Authentication Time (auth_time) claim must be a number present in the ID'\
201
+ ' token when Max Age (max_age) is specified'
202
+ end
203
+
204
+ now = @context[:clock] || Time.now.to_i
205
+ auth_valid_until = claims['auth_time'] + max_age + leeway
206
+
207
+ unless now < auth_valid_until
208
+ raise Auth0::InvalidIdToken, 'Authentication Time (auth_time) claim in the ID token indicates that too'\
209
+ ' much time has passed since the last end-user authentication. Current time'\
210
+ " \"#{now}\" is after last auth at \"#{auth_valid_until}\""
211
+ end
212
+ end
213
+ end
214
+ # rubocop:enable Metrics/ClassLength
215
+
216
+ class JWTAlgorithm
217
+ private_class_method :new
218
+
219
+ def name
220
+ raise RuntimeError, 'Must be overriden by the subclasses'
221
+ end
222
+ end
223
+
224
+ module Algorithm
225
+ # Represents the HS256 algorithm, which rely on shared secrets.
226
+ # @see https://auth0.com/docs/tokens/concepts/signing-algorithms
227
+ class HS256 < JWTAlgorithm
228
+ class << self
229
+ private :new
230
+
231
+ # Create a new instance passing the shared secret.
232
+ # @param secret [string] The HMAC shared secret.
233
+ # @return [HS256] A new instance.
234
+ def secret(secret)
235
+ new secret
236
+ end
237
+ end
238
+
239
+ attr_accessor :secret
240
+
241
+ def initialize(secret)
242
+ raise Auth0::InvalidParameter, 'Must supply a valid secret' if secret.to_s.empty?
243
+
244
+ @secret = secret
245
+ end
246
+
247
+ # Returns the algorithm name.
248
+ # @return [string] The algorithm name.
249
+ def name
250
+ 'HS256'
251
+ end
252
+ end
253
+
254
+ # Represents the RS256 algorithm, which rely on public key certificates.
255
+ # @see https://auth0.com/docs/tokens/concepts/signing-algorithms
256
+ class RS256 < JWTAlgorithm
257
+ include Auth0::Mixins::HTTPProxy
258
+
259
+ @@cache = Zache.new.freeze
260
+
261
+ class << self
262
+ private :new
263
+
264
+ # Create a new instance passing the JWK set url.
265
+ # @param url [string] The url where the JWK set is located.
266
+ # @param lifetime [integer] The lifetime of the JWK set in-memory cache in seconds.
267
+ # Must be a non-negative value. Defaults to *600 seconds* (10 minutes).
268
+ # @return [RS256] A new instance.
269
+ def jwks_url(url, lifetime: 10 * 60)
270
+ new url, lifetime
271
+ end
272
+
273
+ # Clear the JWK set cache.
274
+ def remove_jwks
275
+ @@cache.remove(:jwks)
276
+ end
277
+ end
278
+
279
+ def initialize(jwks_url, lifetime)
280
+ raise Auth0::InvalidParameter, 'Must supply a valid jwks_url' if jwks_url.to_s.empty?
281
+ raise Auth0::InvalidParameter, 'Must supply a valid lifetime' unless lifetime.is_a?(Integer) && lifetime >= 0
282
+
283
+ @lifetime = lifetime
284
+ @jwks_url = jwks_url
285
+ @did_fetch_jwks = false
286
+ end
287
+
288
+ # Returns the algorithm name.
289
+ # @return [string] The algorithm name.
290
+ def name
291
+ 'RS256'
292
+ end
293
+
294
+ # Fetches the JWK set from the in-memory cache or from the url.
295
+ # @return [hash] A JWK set.
296
+ def jwks(force: false)
297
+ result = fetch_jwks if force
298
+
299
+ if result
300
+ @@cache.put(:jwks, result, lifetime: @lifetime)
301
+ return result
302
+ end
303
+
304
+ previous_value = @@cache.last(:jwks)
305
+
306
+ @@cache.get(:jwks, lifetime: @lifetime, dirty: true) do
307
+ new_value = fetch_jwks
308
+
309
+ raise Auth0::InvalidIdToken, 'Could not fetch the JWK set' unless new_value || previous_value
310
+
311
+ new_value || previous_value
312
+ end
313
+ end
314
+
315
+ # Returns whether or not the JWK set was fetched from the url.
316
+ # @return [boolean] +true+ if a request to the JWK set url was made, +false+ otherwise.
317
+ def fetched_jwks?
318
+ @did_fetch_jwks
319
+ end
320
+
321
+ private
322
+
323
+ def fetch_jwks
324
+ result = get(@jwks_url)
325
+ @did_fetch_jwks = result.is_a?(Hash) && result.key?('keys')
326
+ result if @did_fetch_jwks
327
+ end
328
+ end
329
+ end
23
330
  end
24
331
  end
25
332
  end