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.
- checksums.yaml +4 -4
- data/.env.example +2 -0
- data/CHANGELOG.md +55 -0
- data/DEPLOYMENT.md +14 -0
- data/Gemfile.lock +58 -53
- data/README.md +105 -25
- data/auth0.gemspec +2 -0
- data/lib/auth0.rb +1 -0
- data/lib/auth0/algorithm.rb +5 -0
- data/lib/auth0/api/authentication_endpoints.rb +38 -2
- data/lib/auth0/api/v1/clients.rb +12 -2
- data/lib/auth0/api/v1/connections.rb +15 -0
- data/lib/auth0/api/v1/logs.rb +9 -0
- data/lib/auth0/api/v1/rules.rb +12 -0
- data/lib/auth0/api/v1/users.rb +63 -0
- data/lib/auth0/api/v2.rb +4 -0
- data/lib/auth0/api/v2/log_streams.rb +78 -0
- data/lib/auth0/api/v2/prompts.rb +70 -0
- data/lib/auth0/exception.rb +2 -0
- data/lib/auth0/mixins/httpproxy.rb +1 -1
- data/lib/auth0/mixins/validation.rb +307 -0
- data/lib/auth0/version.rb +1 -1
- data/spec/lib/auth0/api/authentication_endpoints_spec.rb +37 -10
- data/spec/lib/auth0/api/v2/log_streams_spec.rb +84 -0
- data/spec/lib/auth0/api/v2/prompts_spec.rb +88 -0
- data/spec/lib/auth0/mixins/validation_spec.rb +466 -0
- data/spec/spec_helper.rb +4 -0
- metadata +43 -5
data/lib/auth0/api/v2.rb
CHANGED
@@ -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
|
data/lib/auth0/exception.rb
CHANGED
@@ -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
|