rodauth-oauth 0.0.2 → 0.1.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/CHANGELOG.md +189 -0
- data/LICENSE.txt +191 -0
- data/README.md +258 -30
- data/lib/generators/roda/oauth/install_generator.rb +1 -1
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -1
- data/lib/generators/roda/oauth/views_generator.rb +1 -6
- data/lib/rodauth/features/oauth.rb +567 -295
- data/lib/rodauth/features/oauth_http_mac.rb +110 -0
- data/lib/rodauth/features/oauth_jwt.rb +448 -0
- data/lib/rodauth/features/oidc.rb +267 -0
- data/lib/rodauth/oauth/ttl_store.rb +59 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +11 -5
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_http_mac) do
|
5
|
+
# :nocov:
|
6
|
+
unless String.method_defined?(:delete_prefix)
|
7
|
+
module PrefixExtensions
|
8
|
+
refine(String) do
|
9
|
+
def delete_suffix(suffix)
|
10
|
+
suffix = suffix.to_s
|
11
|
+
len = suffix.length
|
12
|
+
if len.positive? && index(suffix, -len)
|
13
|
+
self[0...-len]
|
14
|
+
else
|
15
|
+
dup
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete_prefix(prefix)
|
20
|
+
prefix = prefix.to_s
|
21
|
+
if rindex(prefix, 0)
|
22
|
+
self[prefix.length..-1]
|
23
|
+
else
|
24
|
+
dup
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
using(PrefixExtensions)
|
30
|
+
end
|
31
|
+
# :nocov:
|
32
|
+
|
33
|
+
depends :oauth
|
34
|
+
|
35
|
+
auth_value_method :oauth_token_type, "mac"
|
36
|
+
auth_value_method :oauth_mac_algorithm, "hmac-sha-256" # hmac-sha-256, hmac-sha-1
|
37
|
+
auth_value_method :oauth_tokens_mac_key_column, :mac_key
|
38
|
+
|
39
|
+
def authorization_token
|
40
|
+
return @authorization_token if defined?(@authorization_token)
|
41
|
+
|
42
|
+
@authorization_token = begin
|
43
|
+
value = request.get_header("HTTP_AUTHORIZATION").to_s
|
44
|
+
|
45
|
+
scheme, token = value.split(/ +/, 2)
|
46
|
+
|
47
|
+
return unless scheme == "MAC"
|
48
|
+
|
49
|
+
mac_attributes = parse_mac_authorization_header_props(token)
|
50
|
+
|
51
|
+
oauth_token = oauth_token_by_token(mac_attributes["id"])
|
52
|
+
|
53
|
+
return unless oauth_token && mac_signature_matches?(oauth_token, mac_attributes)
|
54
|
+
|
55
|
+
oauth_token
|
56
|
+
|
57
|
+
# TODO: set new MAC-KEY for the next request
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def generate_oauth_token(params = {}, *args)
|
64
|
+
super({ oauth_tokens_mac_key_column => oauth_unique_id_generator }.merge(params), *args)
|
65
|
+
end
|
66
|
+
|
67
|
+
def json_access_token_payload(oauth_token)
|
68
|
+
payload = super
|
69
|
+
|
70
|
+
payload["mac_key"] = oauth_token[oauth_tokens_mac_key_column]
|
71
|
+
payload["mac_algorithm"] = oauth_mac_algorithm
|
72
|
+
|
73
|
+
payload
|
74
|
+
end
|
75
|
+
|
76
|
+
def mac_signature_matches?(oauth_token, mac_attributes)
|
77
|
+
nonce = mac_attributes["nonce"]
|
78
|
+
uri = URI(request.url)
|
79
|
+
|
80
|
+
request_signature = [
|
81
|
+
nonce,
|
82
|
+
request.request_method,
|
83
|
+
uri.request_uri,
|
84
|
+
uri.host,
|
85
|
+
uri.port
|
86
|
+
].join("\n") + ("\n" * 3)
|
87
|
+
|
88
|
+
mac_algorithm = case oauth_mac_algorithm
|
89
|
+
when "hmac-sha-256"
|
90
|
+
OpenSSL::Digest::SHA256
|
91
|
+
when "hmac-sha-1"
|
92
|
+
OpenSSL::Digest::SHA1
|
93
|
+
else
|
94
|
+
raise ArgumentError, "Unsupported algorithm"
|
95
|
+
end
|
96
|
+
|
97
|
+
mac_signature = Base64.strict_encode64 \
|
98
|
+
OpenSSL::HMAC.digest(mac_algorithm.new, oauth_token[oauth_tokens_mac_key_column], request_signature)
|
99
|
+
|
100
|
+
mac_signature == mac_attributes["mac"]
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_mac_authorization_header_props(token)
|
104
|
+
@mac_authorization_header_props = token.split(/ *, */).each_with_object({}) do |prop, props|
|
105
|
+
field, value = prop.split(/ *= */, 2)
|
106
|
+
props[field] = value.delete_prefix("\"").delete_suffix("\"")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,448 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth/ttl_store"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt) do
|
7
|
+
depends :oauth
|
8
|
+
|
9
|
+
auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
|
10
|
+
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
11
|
+
|
12
|
+
auth_value_method :oauth_jwt_token_issuer, nil
|
13
|
+
|
14
|
+
auth_value_method :oauth_application_jws_jwk_column, nil
|
15
|
+
|
16
|
+
auth_value_method :oauth_jwt_key, nil
|
17
|
+
auth_value_method :oauth_jwt_public_key, nil
|
18
|
+
auth_value_method :oauth_jwt_algorithm, "HS256"
|
19
|
+
|
20
|
+
auth_value_method :oauth_jwt_jwe_key, nil
|
21
|
+
auth_value_method :oauth_jwt_jwe_public_key, nil
|
22
|
+
auth_value_method :oauth_jwt_jwe_algorithm, nil
|
23
|
+
auth_value_method :oauth_jwt_jwe_encryption_method, nil
|
24
|
+
|
25
|
+
auth_value_method :oauth_jwt_jwe_copyright, nil
|
26
|
+
auth_value_method :oauth_jwt_audience, nil
|
27
|
+
|
28
|
+
auth_value_method :request_uri_not_supported_message, "request uri is unsupported"
|
29
|
+
auth_value_method :invalid_request_object_message, "request object is invalid"
|
30
|
+
|
31
|
+
auth_value_methods(
|
32
|
+
:jwt_encode,
|
33
|
+
:jwt_decode,
|
34
|
+
:jwks_set,
|
35
|
+
:last_account_login_at
|
36
|
+
)
|
37
|
+
|
38
|
+
JWKS = OAuth::TtlStore.new
|
39
|
+
|
40
|
+
def require_oauth_authorization(*scopes)
|
41
|
+
authorization_required unless authorization_token
|
42
|
+
|
43
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
44
|
+
|
45
|
+
token_scopes = authorization_token["scope"].split(" ")
|
46
|
+
|
47
|
+
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
unless method_defined?(:last_account_login_at)
|
53
|
+
def last_account_login_at
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def authorization_token
|
59
|
+
return @authorization_token if defined?(@authorization_token)
|
60
|
+
|
61
|
+
@authorization_token = begin
|
62
|
+
bearer_token = fetch_access_token
|
63
|
+
|
64
|
+
return unless bearer_token
|
65
|
+
|
66
|
+
jwt_token = jwt_decode(bearer_token)
|
67
|
+
|
68
|
+
return unless jwt_token
|
69
|
+
|
70
|
+
return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) ||
|
71
|
+
(oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
|
72
|
+
!jwt_token["sub"]
|
73
|
+
|
74
|
+
jwt_token
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# /authorize
|
79
|
+
|
80
|
+
def validate_oauth_grant_params
|
81
|
+
# TODO: add support for requst_uri
|
82
|
+
redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
|
83
|
+
|
84
|
+
request_object = param_or_nil("request")
|
85
|
+
|
86
|
+
return super unless request_object && oauth_application
|
87
|
+
|
88
|
+
jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
|
89
|
+
jwk = oauth_application[oauth_application_jws_jwk_column]
|
90
|
+
|
91
|
+
if jwk
|
92
|
+
jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
redirect_response_error("invalid_request_object")
|
96
|
+
end
|
97
|
+
|
98
|
+
claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg])
|
99
|
+
|
100
|
+
redirect_response_error("invalid_request_object") unless claims
|
101
|
+
|
102
|
+
# If signed, the Authorization Request
|
103
|
+
# Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
|
104
|
+
# as members, with their semantics being the same as defined in the JWT
|
105
|
+
# [RFC7519] specification. The value of "aud" should be the value of
|
106
|
+
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
107
|
+
# [RFC8414].
|
108
|
+
claims.delete(:iss)
|
109
|
+
audience = claims.delete(:aud)
|
110
|
+
|
111
|
+
redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
|
112
|
+
|
113
|
+
claims.each do |k, v|
|
114
|
+
request.params[k.to_s] = v
|
115
|
+
end
|
116
|
+
|
117
|
+
super
|
118
|
+
end
|
119
|
+
|
120
|
+
# /token
|
121
|
+
|
122
|
+
def before_token
|
123
|
+
# requset authentication optional for assertions
|
124
|
+
return if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
125
|
+
|
126
|
+
super
|
127
|
+
end
|
128
|
+
|
129
|
+
def validate_oauth_token_params
|
130
|
+
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
131
|
+
redirect_response_error("invalid_client") unless param_or_nil("assertion")
|
132
|
+
else
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def create_oauth_token
|
138
|
+
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
139
|
+
create_oauth_token_from_assertion
|
140
|
+
else
|
141
|
+
super
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_oauth_token_from_assertion
|
146
|
+
claims = jwt_decode(param("assertion"))
|
147
|
+
|
148
|
+
redirect_response_error("invalid_grant") unless claims
|
149
|
+
|
150
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
151
|
+
|
152
|
+
account = account_ds(claims["sub"]).first
|
153
|
+
|
154
|
+
redirect_response_error("invalid_client") unless oauth_application && account
|
155
|
+
|
156
|
+
create_params = {
|
157
|
+
oauth_tokens_account_id_column => claims["sub"],
|
158
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
159
|
+
oauth_tokens_scopes_column => claims["scope"]
|
160
|
+
}
|
161
|
+
|
162
|
+
generate_oauth_token(create_params, false)
|
163
|
+
end
|
164
|
+
|
165
|
+
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
166
|
+
create_params = {
|
167
|
+
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
|
168
|
+
}.merge(params)
|
169
|
+
|
170
|
+
if should_generate_refresh_token
|
171
|
+
refresh_token = oauth_unique_id_generator
|
172
|
+
|
173
|
+
if oauth_tokens_refresh_token_hash_column
|
174
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
175
|
+
else
|
176
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
oauth_token = _generate_oauth_token(create_params)
|
181
|
+
|
182
|
+
claims = jwt_claims(oauth_token)
|
183
|
+
|
184
|
+
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
|
185
|
+
# token data.
|
186
|
+
claims[:scope] = oauth_token[oauth_tokens_scopes_column]
|
187
|
+
|
188
|
+
token = jwt_encode(claims)
|
189
|
+
|
190
|
+
oauth_token[oauth_tokens_token_column] = token
|
191
|
+
oauth_token
|
192
|
+
end
|
193
|
+
|
194
|
+
def jwt_claims(oauth_token)
|
195
|
+
issued_at = Time.now.utc.to_i
|
196
|
+
|
197
|
+
claims = {
|
198
|
+
iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
|
199
|
+
iat: issued_at, # issued at
|
200
|
+
#
|
201
|
+
# sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
|
202
|
+
# access tokens obtained through grants where a resource owner is
|
203
|
+
# involved, such as the authorization code grant, the value of "sub"
|
204
|
+
# SHOULD correspond to the subject identifier of the resource owner.
|
205
|
+
# In case of access tokens obtained through grants where no resource
|
206
|
+
# owner is involved, such as the client credentials grant, the value
|
207
|
+
# of "sub" SHOULD correspond to an identifier the authorization
|
208
|
+
# server uses to indicate the client application.
|
209
|
+
sub: jwt_subject(oauth_token),
|
210
|
+
client_id: oauth_application[oauth_applications_client_id_column],
|
211
|
+
|
212
|
+
exp: issued_at + oauth_token_expires_in,
|
213
|
+
aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
|
214
|
+
}
|
215
|
+
|
216
|
+
claims[:auth_time] = last_account_login_at.utc.to_i if last_account_login_at
|
217
|
+
|
218
|
+
claims
|
219
|
+
end
|
220
|
+
|
221
|
+
def jwt_subject(oauth_token)
|
222
|
+
case oauth_jwt_subject_type
|
223
|
+
when "public"
|
224
|
+
oauth_token[oauth_tokens_account_id_column]
|
225
|
+
when "pairwise"
|
226
|
+
id = oauth_token[oauth_tokens_account_id_column]
|
227
|
+
application_id = oauth_token[oauth_tokens_oauth_application_id_column]
|
228
|
+
Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
|
229
|
+
else
|
230
|
+
raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def oauth_token_by_token(token, *)
|
235
|
+
jwt_decode(token)
|
236
|
+
end
|
237
|
+
|
238
|
+
def json_token_introspect_payload(oauth_token)
|
239
|
+
return { active: false } unless oauth_token
|
240
|
+
|
241
|
+
return super unless oauth_token["sub"] # naive check on whether it's a jwt token
|
242
|
+
|
243
|
+
{
|
244
|
+
active: true,
|
245
|
+
scope: oauth_token["scope"],
|
246
|
+
client_id: oauth_token["client_id"],
|
247
|
+
# username
|
248
|
+
token_type: "access_token",
|
249
|
+
exp: oauth_token["exp"],
|
250
|
+
iat: oauth_token["iat"],
|
251
|
+
nbf: oauth_token["nbf"],
|
252
|
+
sub: oauth_token["sub"],
|
253
|
+
aud: oauth_token["aud"],
|
254
|
+
iss: oauth_token["iss"],
|
255
|
+
jti: oauth_token["jti"]
|
256
|
+
}
|
257
|
+
end
|
258
|
+
|
259
|
+
def oauth_server_metadata_body(path)
|
260
|
+
metadata = super
|
261
|
+
metadata.merge! \
|
262
|
+
jwks_uri: jwks_url,
|
263
|
+
token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
|
264
|
+
metadata
|
265
|
+
end
|
266
|
+
|
267
|
+
def _jwt_key
|
268
|
+
@_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Resource Server only!
|
272
|
+
#
|
273
|
+
# returns the jwks set from the authorization server.
|
274
|
+
def auth_server_jwks_set
|
275
|
+
metadata = authorization_server_metadata
|
276
|
+
|
277
|
+
return unless metadata && (jwks_uri = metadata[:jwks_uri])
|
278
|
+
|
279
|
+
jwks_uri = URI(jwks_uri)
|
280
|
+
|
281
|
+
jwks = JWKS[jwks_uri]
|
282
|
+
|
283
|
+
return jwks if jwks
|
284
|
+
|
285
|
+
JWKS.set(jwks_uri) do
|
286
|
+
http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
|
287
|
+
http.use_ssl = jwks_uri.scheme == "https"
|
288
|
+
|
289
|
+
request = Net::HTTP::Get.new(jwks_uri.request_uri)
|
290
|
+
request["accept"] = json_response_content_type
|
291
|
+
response = http.request(request)
|
292
|
+
authorization_required unless response.code.to_i == 200
|
293
|
+
|
294
|
+
# time-to-live
|
295
|
+
ttl = if response.key?("cache-control")
|
296
|
+
cache_control = response["cache_control"]
|
297
|
+
cache_control[/max-age=(\d+)/, 1]
|
298
|
+
elsif response.key?("expires")
|
299
|
+
Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
|
300
|
+
end
|
301
|
+
|
302
|
+
[JSON.parse(response.body, symbolize_names: true), ttl]
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
if defined?(JSON::JWT)
|
307
|
+
# :nocov:
|
308
|
+
|
309
|
+
def jwk_import(data)
|
310
|
+
JSON::JWK.new(data)
|
311
|
+
end
|
312
|
+
|
313
|
+
# json-jwt
|
314
|
+
def jwt_encode(payload)
|
315
|
+
jwt = JSON::JWT.new(payload)
|
316
|
+
jwk = JSON::JWK.new(_jwt_key)
|
317
|
+
|
318
|
+
jwt = jwt.sign(jwk, oauth_jwt_algorithm)
|
319
|
+
jwt.kid = jwk.thumbprint
|
320
|
+
|
321
|
+
if oauth_jwt_jwe_key
|
322
|
+
algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
|
323
|
+
jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
324
|
+
algorithm,
|
325
|
+
oauth_jwt_jwe_encryption_method.to_sym)
|
326
|
+
end
|
327
|
+
jwt.to_s
|
328
|
+
end
|
329
|
+
|
330
|
+
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
|
331
|
+
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
332
|
+
|
333
|
+
@jwt_token = if jws_key
|
334
|
+
JSON::JWT.decode(token, jws_key)
|
335
|
+
elsif !is_authorization_server? && auth_server_jwks_set
|
336
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new(auth_server_jwks_set))
|
337
|
+
end
|
338
|
+
rescue JSON::JWT::Exception
|
339
|
+
nil
|
340
|
+
end
|
341
|
+
|
342
|
+
def jwks_set
|
343
|
+
[
|
344
|
+
(JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
345
|
+
(JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
346
|
+
].compact
|
347
|
+
end
|
348
|
+
|
349
|
+
# :nocov:
|
350
|
+
elsif defined?(JWT)
|
351
|
+
|
352
|
+
# ruby-jwt
|
353
|
+
|
354
|
+
def jwk_import(data)
|
355
|
+
JWT::JWK.import(data).keypair
|
356
|
+
end
|
357
|
+
|
358
|
+
def jwt_encode(payload)
|
359
|
+
headers = {}
|
360
|
+
|
361
|
+
key = _jwt_key
|
362
|
+
|
363
|
+
if key.is_a?(OpenSSL::PKey::RSA)
|
364
|
+
jwk = JWT::JWK.new(_jwt_key)
|
365
|
+
headers[:kid] = jwk.kid
|
366
|
+
|
367
|
+
key = jwk.keypair
|
368
|
+
end
|
369
|
+
|
370
|
+
# Use the key and iat to create a unique key per request to prevent replay attacks
|
371
|
+
jti_raw = [key, payload[:iat]].join(":").to_s
|
372
|
+
jti = Digest::SHA256.hexdigest(jti_raw)
|
373
|
+
|
374
|
+
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
375
|
+
payload[:jti] = jti
|
376
|
+
token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
|
377
|
+
|
378
|
+
if oauth_jwt_jwe_key
|
379
|
+
params = {
|
380
|
+
zip: "DEF",
|
381
|
+
copyright: oauth_jwt_jwe_copyright
|
382
|
+
}
|
383
|
+
params[:enc] = oauth_jwt_jwe_encryption_method if oauth_jwt_jwe_encryption_method
|
384
|
+
params[:alg] = oauth_jwt_jwe_algorithm if oauth_jwt_jwe_algorithm
|
385
|
+
token = JWE.encrypt(token, oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, **params)
|
386
|
+
end
|
387
|
+
|
388
|
+
token
|
389
|
+
end
|
390
|
+
|
391
|
+
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
|
392
|
+
# decrypt jwe
|
393
|
+
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
394
|
+
|
395
|
+
# decode jwt
|
396
|
+
@jwt_token = if jws_key
|
397
|
+
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
|
398
|
+
elsif !is_authorization_server? && auth_server_jwks_set
|
399
|
+
algorithms = auth_server_jwks_set[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
400
|
+
JWT.decode(token, nil, true, jwks: auth_server_jwks_set, algorithms: algorithms).first
|
401
|
+
end
|
402
|
+
rescue JWT::DecodeError, JWT::JWKError
|
403
|
+
nil
|
404
|
+
end
|
405
|
+
|
406
|
+
def jwks_set
|
407
|
+
[
|
408
|
+
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
409
|
+
(JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
410
|
+
].compact
|
411
|
+
end
|
412
|
+
else
|
413
|
+
# :nocov:
|
414
|
+
def jwk_import(_data)
|
415
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
416
|
+
end
|
417
|
+
|
418
|
+
def jwt_encode(_token)
|
419
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
420
|
+
end
|
421
|
+
|
422
|
+
def jwt_decode(_token, **)
|
423
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
424
|
+
end
|
425
|
+
|
426
|
+
def jwks_set
|
427
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
428
|
+
end
|
429
|
+
# :nocov:
|
430
|
+
end
|
431
|
+
|
432
|
+
def validate_oauth_revoke_params
|
433
|
+
token_hint = param_or_nil("token_type_hint")
|
434
|
+
|
435
|
+
throw(:rodauth_error) if !token_hint || token_hint == "access_token"
|
436
|
+
|
437
|
+
super
|
438
|
+
end
|
439
|
+
|
440
|
+
route(:jwks) do |r|
|
441
|
+
next unless is_authorization_server?
|
442
|
+
|
443
|
+
r.get do
|
444
|
+
json_response_success({ keys: jwks_set })
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|