rodauth-oauth 0.10.4 → 1.0.0.pre.beta1
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/MIGRATION-GUIDE-v1.md +286 -0
- data/README.md +22 -30
- data/doc/release_notes/1_0_0_beta1.md +38 -0
- data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +4 -6
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +18 -29
- data/lib/rodauth/features/oauth_application_management.rb +59 -72
- data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +35 -88
- data/lib/rodauth/features/oauth_authorize_base.rb +103 -20
- data/lib/rodauth/features/oauth_base.rb +365 -302
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
- data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +46 -28
- data/lib/rodauth/features/oauth_grant_management.rb +70 -0
- data/lib/rodauth/features/oauth_implicit_grant.rb +25 -24
- data/lib/rodauth/features/oauth_jwt.rb +52 -688
- data/lib/rodauth/features/oauth_jwt_base.rb +435 -0
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +45 -17
- data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +62 -0
- data/lib/rodauth/features/oauth_management_base.rb +2 -0
- data/lib/rodauth/features/oauth_pkce.rb +22 -26
- data/lib/rodauth/features/oauth_resource_indicators.rb +33 -21
- data/lib/rodauth/features/oauth_resource_server.rb +59 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +5 -1
- data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
- data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
- data/lib/rodauth/features/oidc.rb +188 -95
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +89 -53
- data/lib/rodauth/oauth/database_extensions.rb +8 -6
- data/lib/rodauth/oauth/http_extensions.rb +61 -0
- data/lib/rodauth/oauth/railtie.rb +20 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/lib/rodauth/oauth.rb +29 -1
- data/locales/en.yml +32 -22
- data/locales/pt.yml +32 -22
- data/templates/authorize.str +19 -24
- data/templates/device_search.str +1 -1
- data/templates/device_verification.str +2 -2
- data/templates/jwks_field.str +1 -0
- data/templates/new_oauth_application.str +1 -2
- data/templates/oauth_application.str +2 -2
- data/templates/oauth_application_oauth_grants.str +54 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_grants.str +52 -0
- metadata +20 -16
- data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
- data/lib/rodauth/features/oauth.rb +0 -9
- data/lib/rodauth/features/oauth_http_mac.rb +0 -86
- data/lib/rodauth/features/oauth_token_management.rb +0 -81
- data/lib/rodauth/oauth/refinements.rb +0 -48
- data/templates/jwt_public_key_field.str +0 -4
- data/templates/oauth_application_oauth_tokens.str +0 -52
- data/templates/oauth_tokens.str +0 -50
@@ -1,89 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "rodauth/oauth
|
4
|
-
require "rodauth/oauth/
|
3
|
+
require "rodauth/oauth"
|
4
|
+
require "rodauth/oauth/http_extensions"
|
5
5
|
|
6
6
|
module Rodauth
|
7
7
|
Feature.define(:oauth_jwt, :OauthJwt) do
|
8
|
-
depends :
|
8
|
+
depends :oauth_jwt_base, :oauth_jwt_jwks
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
# Recommended to have hmac_secret as well
|
13
|
-
|
14
|
-
auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
|
15
|
-
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
16
|
-
|
17
|
-
auth_value_method :oauth_jwt_token_issuer, nil
|
18
|
-
|
19
|
-
configuration_module_eval do
|
20
|
-
define_method :oauth_applications_jws_jwk_column do
|
21
|
-
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
|
22
|
-
oauth_applications_jwks_column
|
23
|
-
end
|
24
|
-
define_method :oauth_applications_jws_jwk_label do
|
25
|
-
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
|
26
|
-
oauth_applications_jws_jwk_label
|
27
|
-
end
|
28
|
-
define_method :oauth_application_jws_jwk_param do
|
29
|
-
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
|
30
|
-
oauth_applications_jwks_param
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
auth_value_method :oauth_applications_subject_type_column, :subject_type
|
35
|
-
auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
|
36
|
-
auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
|
37
|
-
auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
|
38
|
-
auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
|
39
|
-
|
40
|
-
translatable_method :oauth_applications_jwt_public_key_label, "Public key"
|
41
|
-
|
42
|
-
auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
|
43
|
-
auth_value_method :oauth_application_jwks_param, "jwks"
|
44
|
-
|
45
|
-
auth_value_method :oauth_jwt_keys, {}
|
46
|
-
auth_value_method :oauth_jwt_key, nil
|
47
|
-
auth_value_method :oauth_jwt_public_keys, {}
|
48
|
-
auth_value_method :oauth_jwt_public_key, nil
|
49
|
-
auth_value_method :oauth_jwt_algorithm, "RS256"
|
50
|
-
|
51
|
-
auth_value_method :oauth_jwt_jwe_keys, {}
|
52
|
-
auth_value_method :oauth_jwt_jwe_key, nil
|
53
|
-
auth_value_method :oauth_jwt_jwe_public_keys, {}
|
54
|
-
auth_value_method :oauth_jwt_jwe_public_key, nil
|
55
|
-
auth_value_method :oauth_jwt_jwe_algorithm, nil
|
56
|
-
auth_value_method :oauth_jwt_jwe_encryption_method, nil
|
57
|
-
|
58
|
-
# values used for rotating keys
|
59
|
-
auth_value_method :oauth_jwt_legacy_public_key, nil
|
60
|
-
auth_value_method :oauth_jwt_legacy_algorithm, nil
|
61
|
-
|
62
|
-
auth_value_method :oauth_jwt_jwe_copyright, nil
|
63
|
-
auth_value_method :oauth_jwt_audience, nil
|
64
|
-
|
65
|
-
translatable_method :request_uri_not_supported_message, "request uri is unsupported"
|
66
|
-
translatable_method :invalid_request_object_message, "request object is invalid"
|
67
|
-
|
68
|
-
auth_value_methods(
|
69
|
-
:jwt_encode,
|
70
|
-
:jwt_decode,
|
71
|
-
:jwks_set,
|
72
|
-
:generate_jti
|
73
|
-
)
|
74
|
-
|
75
|
-
route(:jwks) do |r|
|
76
|
-
next unless is_authorization_server?
|
77
|
-
|
78
|
-
r.get do
|
79
|
-
json_response_success({ keys: jwks_set }, true)
|
80
|
-
end
|
81
|
-
end
|
10
|
+
auth_value_method :oauth_jwt_access_tokens, true
|
82
11
|
|
83
12
|
def require_oauth_authorization(*scopes)
|
84
|
-
|
13
|
+
return super unless oauth_jwt_access_tokens
|
85
14
|
|
86
|
-
|
15
|
+
authorization_required unless authorization_token
|
87
16
|
|
88
17
|
token_scopes = authorization_token["scope"].split(" ")
|
89
18
|
|
@@ -91,18 +20,32 @@ module Rodauth
|
|
91
20
|
end
|
92
21
|
|
93
22
|
def oauth_token_subject
|
23
|
+
return super unless oauth_jwt_access_tokens
|
24
|
+
|
94
25
|
return unless authorization_token
|
95
26
|
|
96
27
|
authorization_token["sub"]
|
97
28
|
end
|
98
29
|
|
99
|
-
|
30
|
+
def current_oauth_account
|
31
|
+
subject = oauth_token_subject
|
32
|
+
|
33
|
+
return if subject == authorization_token["client_id"]
|
34
|
+
|
35
|
+
oauth_account_ds(subject).first
|
36
|
+
end
|
100
37
|
|
101
|
-
def
|
102
|
-
|
38
|
+
def current_oauth_application
|
39
|
+
db[oauth_applications_table].where(
|
40
|
+
oauth_applications_client_id_column => authorization_token["client_id"]
|
41
|
+
).first
|
103
42
|
end
|
104
43
|
|
44
|
+
private
|
45
|
+
|
105
46
|
def authorization_token
|
47
|
+
return super unless oauth_jwt_access_tokens
|
48
|
+
|
106
49
|
return @authorization_token if defined?(@authorization_token)
|
107
50
|
|
108
51
|
@authorization_token = begin
|
@@ -110,97 +53,58 @@ module Rodauth
|
|
110
53
|
|
111
54
|
return unless bearer_token
|
112
55
|
|
113
|
-
|
114
|
-
|
115
|
-
return unless jwt_token
|
116
|
-
|
117
|
-
return if jwt_token["iss"] != issuer ||
|
118
|
-
(oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
|
119
|
-
!jwt_token["sub"]
|
120
|
-
|
121
|
-
jwt_token
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
# /authorize
|
56
|
+
jwt_claims = jwt_decode(bearer_token)
|
126
57
|
|
127
|
-
|
128
|
-
# TODO: add support for requst_uri
|
129
|
-
redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
|
58
|
+
return unless jwt_claims
|
130
59
|
|
131
|
-
|
60
|
+
return unless jwt_claims["sub"]
|
132
61
|
|
133
|
-
|
62
|
+
return unless jwt_claims["aud"]
|
134
63
|
|
135
|
-
|
136
|
-
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
137
|
-
else
|
138
|
-
redirect_response_error("invalid_request_object")
|
64
|
+
jwt_claims
|
139
65
|
end
|
140
|
-
|
141
|
-
request_sig_enc_opts = {
|
142
|
-
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
143
|
-
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
144
|
-
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
145
|
-
}.compact
|
146
|
-
|
147
|
-
claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
|
148
|
-
|
149
|
-
redirect_response_error("invalid_request_object") unless claims
|
150
|
-
|
151
|
-
# If signed, the Authorization Request
|
152
|
-
# Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
|
153
|
-
# as members, with their semantics being the same as defined in the JWT
|
154
|
-
# [RFC7519] specification. The value of "aud" should be the value of
|
155
|
-
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
156
|
-
# [RFC8414].
|
157
|
-
claims.delete("iss")
|
158
|
-
audience = claims.delete("aud")
|
159
|
-
|
160
|
-
redirect_response_error("invalid_request_object") if audience && audience != issuer
|
161
|
-
|
162
|
-
claims.each do |k, v|
|
163
|
-
request.params[k.to_s] = v
|
164
|
-
end
|
165
|
-
|
166
|
-
super
|
167
66
|
end
|
168
67
|
|
169
68
|
# /token
|
170
69
|
|
171
|
-
def
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
70
|
+
def create_token_from_token(_grant, update_params)
|
71
|
+
oauth_grant = super
|
72
|
+
|
73
|
+
if oauth_jwt_access_tokens
|
74
|
+
access_token = _generate_jwt_access_token(oauth_grant)
|
75
|
+
oauth_grant[oauth_grants_token_column] = access_token
|
76
|
+
end
|
77
|
+
oauth_grant
|
176
78
|
end
|
177
79
|
|
178
|
-
def
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
80
|
+
def generate_token(_grant_params = {}, should_generate_refresh_token = true)
|
81
|
+
oauth_grant = super
|
82
|
+
if oauth_jwt_access_tokens
|
83
|
+
access_token = _generate_jwt_access_token(oauth_grant)
|
84
|
+
oauth_grant[oauth_grants_token_column] = access_token
|
85
|
+
end
|
86
|
+
oauth_grant
|
183
87
|
end
|
184
88
|
|
185
|
-
def _generate_jwt_access_token(
|
186
|
-
claims = jwt_claims(
|
89
|
+
def _generate_jwt_access_token(oauth_grant)
|
90
|
+
claims = jwt_claims(oauth_grant)
|
187
91
|
|
188
92
|
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
|
189
93
|
# token data.
|
190
|
-
claims[:scope] =
|
94
|
+
claims[:scope] = oauth_grant[oauth_grants_scopes_column]
|
191
95
|
|
192
96
|
jwt_encode(claims)
|
193
97
|
end
|
194
98
|
|
195
99
|
def _generate_access_token(*)
|
196
|
-
|
100
|
+
return super unless oauth_jwt_access_tokens
|
197
101
|
end
|
198
102
|
|
199
|
-
def jwt_claims(
|
103
|
+
def jwt_claims(oauth_grant)
|
200
104
|
issued_at = Time.now.to_i
|
201
105
|
|
202
106
|
{
|
203
|
-
iss:
|
107
|
+
iss: oauth_jwt_issuer, # issuer
|
204
108
|
iat: issued_at, # issued at
|
205
109
|
#
|
206
110
|
# sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
|
@@ -211,552 +115,12 @@ module Rodauth
|
|
211
115
|
# owner is involved, such as the client credentials grant, the value
|
212
116
|
# of "sub" SHOULD correspond to an identifier the authorization
|
213
117
|
# server uses to indicate the client application.
|
214
|
-
sub: jwt_subject(
|
118
|
+
sub: jwt_subject(oauth_grant),
|
215
119
|
client_id: oauth_application[oauth_applications_client_id_column],
|
216
120
|
|
217
|
-
exp: issued_at +
|
218
|
-
aud:
|
121
|
+
exp: issued_at + oauth_access_token_expires_in,
|
122
|
+
aud: oauth_jwt_audience
|
219
123
|
}
|
220
124
|
end
|
221
|
-
|
222
|
-
def jwt_subject(oauth_token)
|
223
|
-
subject_type = if oauth_application
|
224
|
-
oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
|
225
|
-
else
|
226
|
-
oauth_jwt_subject_type
|
227
|
-
end
|
228
|
-
case subject_type
|
229
|
-
when "public"
|
230
|
-
oauth_token[oauth_tokens_account_id_column]
|
231
|
-
when "pairwise"
|
232
|
-
id = oauth_token[oauth_tokens_account_id_column]
|
233
|
-
application_id = oauth_token[oauth_tokens_oauth_application_id_column]
|
234
|
-
Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
|
235
|
-
else
|
236
|
-
raise StandardError, "unexpected subject (#{subject_type})"
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
def oauth_token_by_token(token)
|
241
|
-
jwt_decode(token)
|
242
|
-
end
|
243
|
-
|
244
|
-
def token_from_application?(grant_or_claims, oauth_application)
|
245
|
-
return super if grant_or_claims[oauth_tokens_id_column]
|
246
|
-
|
247
|
-
if grant_or_claims["client_id"]
|
248
|
-
grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
|
249
|
-
else
|
250
|
-
Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def json_token_introspect_payload(oauth_token)
|
255
|
-
return { active: false } unless oauth_token
|
256
|
-
|
257
|
-
return super unless oauth_token["sub"] # naive check on whether it's a jwt token
|
258
|
-
|
259
|
-
{
|
260
|
-
active: true,
|
261
|
-
scope: oauth_token["scope"],
|
262
|
-
client_id: oauth_token["client_id"],
|
263
|
-
# username
|
264
|
-
token_type: "access_token",
|
265
|
-
exp: oauth_token["exp"],
|
266
|
-
iat: oauth_token["iat"],
|
267
|
-
nbf: oauth_token["nbf"],
|
268
|
-
sub: oauth_token["sub"],
|
269
|
-
aud: oauth_token["aud"],
|
270
|
-
iss: oauth_token["iss"],
|
271
|
-
jti: oauth_token["jti"]
|
272
|
-
}
|
273
|
-
end
|
274
|
-
|
275
|
-
def oauth_server_metadata_body(path = nil)
|
276
|
-
metadata = super
|
277
|
-
metadata.merge! \
|
278
|
-
jwks_uri: jwks_url,
|
279
|
-
token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
|
280
|
-
metadata
|
281
|
-
end
|
282
|
-
|
283
|
-
def _jwt_key
|
284
|
-
@_jwt_key ||= oauth_jwt_key || begin
|
285
|
-
if oauth_application
|
286
|
-
|
287
|
-
if (jwks = oauth_application_jwks)
|
288
|
-
jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
|
289
|
-
jwks
|
290
|
-
else
|
291
|
-
oauth_application[oauth_applications_jwt_public_key_column]
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
def _jwt_public_key
|
298
|
-
@_jwt_public_key ||= oauth_jwt_public_key || begin
|
299
|
-
if oauth_application
|
300
|
-
jwks || oauth_application[oauth_applications_jwt_public_key_column]
|
301
|
-
else
|
302
|
-
_jwt_key
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
# Resource Server only!
|
308
|
-
#
|
309
|
-
# returns the jwks set from the authorization server.
|
310
|
-
def auth_server_jwks_set
|
311
|
-
metadata = authorization_server_metadata
|
312
|
-
|
313
|
-
return unless metadata && (jwks_uri = metadata[:jwks_uri])
|
314
|
-
|
315
|
-
jwks_uri = URI(jwks_uri)
|
316
|
-
|
317
|
-
jwks = JWKS[jwks_uri]
|
318
|
-
|
319
|
-
return jwks if jwks
|
320
|
-
|
321
|
-
JWKS.set(jwks_uri) do
|
322
|
-
http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
|
323
|
-
http.use_ssl = jwks_uri.scheme == "https"
|
324
|
-
|
325
|
-
request = Net::HTTP::Get.new(jwks_uri.request_uri)
|
326
|
-
request["accept"] = json_response_content_type
|
327
|
-
response = http.request(request)
|
328
|
-
authorization_required unless response.code.to_i == 200
|
329
|
-
|
330
|
-
# time-to-live
|
331
|
-
ttl = if response.key?("cache-control")
|
332
|
-
cache_control = response["cache-control"]
|
333
|
-
cache_control[/max-age=(\d+)/, 1].to_i
|
334
|
-
elsif response.key?("expires")
|
335
|
-
Time.parse(response["expires"]).to_i - Time.now.to_i
|
336
|
-
end
|
337
|
-
|
338
|
-
[JSON.parse(response.body, symbolize_names: true), ttl]
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
def generate_jti(payload)
|
343
|
-
# Use the key and iat to create a unique key per request to prevent replay attacks
|
344
|
-
jti_raw = [
|
345
|
-
payload[:aud] || payload["aud"],
|
346
|
-
payload[:iat] || payload["iat"]
|
347
|
-
].join(":").to_s
|
348
|
-
Digest::SHA256.hexdigest(jti_raw)
|
349
|
-
end
|
350
|
-
|
351
|
-
def verify_jti(jti, claims)
|
352
|
-
generate_jti(claims) == jti
|
353
|
-
end
|
354
|
-
|
355
|
-
def verify_aud(expected_aud, aud)
|
356
|
-
expected_aud == aud
|
357
|
-
end
|
358
|
-
|
359
|
-
def oauth_application_jwks
|
360
|
-
jwks = oauth_application[oauth_applications_jwks_column]
|
361
|
-
|
362
|
-
if jwks
|
363
|
-
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
364
|
-
return jwks
|
365
|
-
end
|
366
|
-
|
367
|
-
jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
|
368
|
-
|
369
|
-
return unless jwks_uri
|
370
|
-
|
371
|
-
jwks_uri = URI(jwks_uri)
|
372
|
-
|
373
|
-
jwks = JWKS[jwks_uri]
|
374
|
-
|
375
|
-
return jwks if jwks
|
376
|
-
|
377
|
-
JWKS.set(jwks_uri) do
|
378
|
-
http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
|
379
|
-
http.use_ssl = jwks_uri.scheme == "https"
|
380
|
-
|
381
|
-
request = Net::HTTP::Get.new(jwks_uri.request_uri)
|
382
|
-
request["accept"] = json_response_content_type
|
383
|
-
response = http.request(request)
|
384
|
-
return unless response.code.to_i == 200
|
385
|
-
|
386
|
-
# time-to-live
|
387
|
-
ttl = if response.key?("cache-control")
|
388
|
-
cache_control = response["cache-control"]
|
389
|
-
cache_control[/max-age=(\d+)/, 1].to_i
|
390
|
-
elsif response.key?("expires")
|
391
|
-
Time.parse(response["expires"]).to_i - Time.now.to_i
|
392
|
-
end
|
393
|
-
|
394
|
-
[JSON.parse(response.body, symbolize_names: true), ttl]
|
395
|
-
end
|
396
|
-
end
|
397
|
-
|
398
|
-
if defined?(JSON::JWT)
|
399
|
-
# json-jwt
|
400
|
-
|
401
|
-
auth_value_method :oauth_jwt_algorithms_supported, %w[
|
402
|
-
HS256 HS384 HS512
|
403
|
-
RS256 RS384 RS512
|
404
|
-
PS256 PS384 PS512
|
405
|
-
ES256 ES384 ES512 ES256K
|
406
|
-
]
|
407
|
-
auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
|
408
|
-
RSA1_5 RSA-OAEP dir A128KW A256KW
|
409
|
-
]
|
410
|
-
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
|
411
|
-
A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
|
412
|
-
]
|
413
|
-
|
414
|
-
def jwk_import(data)
|
415
|
-
JSON::JWK.new(data)
|
416
|
-
end
|
417
|
-
|
418
|
-
def jwt_encode(payload,
|
419
|
-
jwks: nil,
|
420
|
-
encryption_algorithm: oauth_jwt_jwe_algorithm,
|
421
|
-
encryption_method: oauth_jwt_jwe_encryption_method,
|
422
|
-
jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
|
423
|
-
encryption_method]] || oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
424
|
-
signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
|
425
|
-
payload[:jti] = generate_jti(payload)
|
426
|
-
jwt = JSON::JWT.new(payload)
|
427
|
-
|
428
|
-
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
429
|
-
key = key.first if key.is_a?(Array)
|
430
|
-
|
431
|
-
jwk = JSON::JWK.new(key || "")
|
432
|
-
|
433
|
-
jwt = jwt.sign(jwk, signing_algorithm)
|
434
|
-
jwt.kid = jwk.thumbprint
|
435
|
-
|
436
|
-
if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
|
437
|
-
jwk = JSON::JWK.new(jwk)
|
438
|
-
jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
|
439
|
-
jwe.to_s
|
440
|
-
elsif jwe_key
|
441
|
-
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
442
|
-
algorithm = encryption_algorithm.to_sym if encryption_algorithm
|
443
|
-
meth = encryption_method.to_sym if encryption_method
|
444
|
-
jwt.encrypt(jwe_key, algorithm, meth)
|
445
|
-
else
|
446
|
-
jwt.to_s
|
447
|
-
end
|
448
|
-
end
|
449
|
-
|
450
|
-
def jwt_decode(
|
451
|
-
token,
|
452
|
-
jwks: nil,
|
453
|
-
jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
|
454
|
-
jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
|
455
|
-
jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
|
456
|
-
jws_encryption_method: oauth_jwt_jwe_encryption_method,
|
457
|
-
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
|
458
|
-
verify_claims: true,
|
459
|
-
verify_jti: true,
|
460
|
-
verify_iss: true,
|
461
|
-
verify_aud: false,
|
462
|
-
**
|
463
|
-
)
|
464
|
-
jws_key = jws_key.first if jws_key.is_a?(Array)
|
465
|
-
|
466
|
-
if jwe_key
|
467
|
-
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
468
|
-
token = JSON::JWT.decode(token, jwe_key).plain_text
|
469
|
-
end
|
470
|
-
|
471
|
-
claims = if is_authorization_server?
|
472
|
-
if oauth_jwt_legacy_public_key
|
473
|
-
JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
|
474
|
-
elsif jwks
|
475
|
-
enc_algs = [jws_encryption_algorithm].compact
|
476
|
-
enc_meths = [jws_encryption_method].compact
|
477
|
-
sig_algs = [jws_algorithm].compact.map(&:to_sym)
|
478
|
-
jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
|
479
|
-
jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
|
480
|
-
jws
|
481
|
-
elsif jws_key
|
482
|
-
JSON::JWT.decode(token, jws_key)
|
483
|
-
end
|
484
|
-
elsif (jwks = auth_server_jwks_set)
|
485
|
-
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
486
|
-
end
|
487
|
-
|
488
|
-
now = Time.now
|
489
|
-
if verify_claims && (
|
490
|
-
(!claims[:exp] || Time.at(claims[:exp]) < now) &&
|
491
|
-
(claims[:nbf] && Time.at(claims[:nbf]) < now) &&
|
492
|
-
(claims[:iat] && Time.at(claims[:iat]) < now) &&
|
493
|
-
(verify_iss && claims[:iss] != issuer) &&
|
494
|
-
(verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
|
495
|
-
(verify_jti && !verify_jti(claims[:jti], claims))
|
496
|
-
)
|
497
|
-
return
|
498
|
-
end
|
499
|
-
|
500
|
-
claims
|
501
|
-
rescue JSON::JWT::Exception
|
502
|
-
nil
|
503
|
-
end
|
504
|
-
|
505
|
-
def jwks_set
|
506
|
-
@jwks_set ||= [
|
507
|
-
*(
|
508
|
-
unless oauth_jwt_public_keys.empty?
|
509
|
-
oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JSON::JWK.new(pkey).merge(use: "sig", alg: algo) } }
|
510
|
-
end
|
511
|
-
),
|
512
|
-
*(
|
513
|
-
unless oauth_jwt_jwe_public_keys.empty?
|
514
|
-
oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
|
515
|
-
pkeys.map do |pkey|
|
516
|
-
JSON::JWK.new(pkey).merge(use: "enc", alg: algo)
|
517
|
-
end
|
518
|
-
end
|
519
|
-
end
|
520
|
-
),
|
521
|
-
# legacy
|
522
|
-
(JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
523
|
-
(JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
|
524
|
-
(JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
525
|
-
].compact
|
526
|
-
end
|
527
|
-
|
528
|
-
elsif defined?(JWT)
|
529
|
-
# ruby-jwt
|
530
|
-
require "rodauth/oauth/jwe_extensions" if defined?(JWE)
|
531
|
-
|
532
|
-
auth_value_method :oauth_jwt_algorithms_supported, %w[
|
533
|
-
HS256 HS384 HS512 HS512256
|
534
|
-
RS256 RS384 RS512
|
535
|
-
ED25519
|
536
|
-
ES256 ES384 ES512
|
537
|
-
PS256 PS384 PS512
|
538
|
-
]
|
539
|
-
|
540
|
-
auth_value_methods(
|
541
|
-
:oauth_jwt_jwe_algorithms_supported,
|
542
|
-
:oauth_jwt_jwe_encryption_methods_supported
|
543
|
-
)
|
544
|
-
|
545
|
-
def oauth_jwt_jwe_algorithms_supported
|
546
|
-
JWE::VALID_ALG
|
547
|
-
end
|
548
|
-
|
549
|
-
def oauth_jwt_jwe_encryption_methods_supported
|
550
|
-
JWE::VALID_ENC
|
551
|
-
end
|
552
|
-
|
553
|
-
def jwk_import(data)
|
554
|
-
JWT::JWK.import(data).keypair
|
555
|
-
end
|
556
|
-
|
557
|
-
def jwt_encode(payload,
|
558
|
-
signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
|
559
|
-
headers = {}
|
560
|
-
|
561
|
-
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
562
|
-
key = key.first if key.is_a?(Array)
|
563
|
-
|
564
|
-
case key
|
565
|
-
when OpenSSL::PKey::PKey
|
566
|
-
jwk = JWT::JWK.new(key)
|
567
|
-
headers[:kid] = jwk.kid
|
568
|
-
|
569
|
-
key = jwk.keypair
|
570
|
-
end
|
571
|
-
|
572
|
-
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
573
|
-
payload[:jti] = generate_jti(payload)
|
574
|
-
JWT.encode(payload, key, signing_algorithm, headers)
|
575
|
-
end
|
576
|
-
|
577
|
-
if defined?(JWE)
|
578
|
-
def jwt_encode_with_jwe(
|
579
|
-
payload,
|
580
|
-
jwks: nil,
|
581
|
-
encryption_algorithm: oauth_jwt_jwe_algorithm,
|
582
|
-
encryption_method: oauth_jwt_jwe_encryption_method,
|
583
|
-
jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]] || oauth_jwt_jwe_key,
|
584
|
-
**args
|
585
|
-
)
|
586
|
-
token = jwt_encode_without_jwe(payload, **args)
|
587
|
-
|
588
|
-
return token unless encryption_algorithm && encryption_method
|
589
|
-
|
590
|
-
if jwks && jwks.any? { |k| k[:use] == "enc" }
|
591
|
-
JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
|
592
|
-
elsif jwe_key
|
593
|
-
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
594
|
-
params = {
|
595
|
-
zip: "DEF",
|
596
|
-
copyright: oauth_jwt_jwe_copyright
|
597
|
-
}
|
598
|
-
params[:enc] = encryption_method if encryption_method
|
599
|
-
params[:alg] = encryption_algorithm if encryption_algorithm
|
600
|
-
JWE.encrypt(token, jwe_key, **params)
|
601
|
-
else
|
602
|
-
token
|
603
|
-
end
|
604
|
-
end
|
605
|
-
|
606
|
-
alias_method :jwt_encode_without_jwe, :jwt_encode
|
607
|
-
alias_method :jwt_encode, :jwt_encode_with_jwe
|
608
|
-
end
|
609
|
-
|
610
|
-
def jwt_decode(
|
611
|
-
token,
|
612
|
-
jwks: nil,
|
613
|
-
jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
|
614
|
-
jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
|
615
|
-
verify_claims: true,
|
616
|
-
verify_jti: true,
|
617
|
-
verify_iss: true,
|
618
|
-
verify_aud: false
|
619
|
-
)
|
620
|
-
jws_key = jws_key.first if jws_key.is_a?(Array)
|
621
|
-
|
622
|
-
# verifying the JWT implies verifying:
|
623
|
-
#
|
624
|
-
# issuer: check that server generated the token
|
625
|
-
# aud: check the audience field (client is who he says he is)
|
626
|
-
# iat: check that the token didn't expire
|
627
|
-
#
|
628
|
-
# subject can't be verified automatically without having access to the account id,
|
629
|
-
# which we don't because that's the whole point.
|
630
|
-
#
|
631
|
-
verify_claims_params = if verify_claims
|
632
|
-
{
|
633
|
-
verify_iss: verify_iss,
|
634
|
-
iss: issuer,
|
635
|
-
# can't use stock aud verification, as it's dependent on the client application id
|
636
|
-
verify_aud: false,
|
637
|
-
verify_jti: (verify_jti ? method(:verify_jti) : false),
|
638
|
-
verify_iat: true
|
639
|
-
}
|
640
|
-
else
|
641
|
-
{}
|
642
|
-
end
|
643
|
-
|
644
|
-
# decode jwt
|
645
|
-
claims = if is_authorization_server?
|
646
|
-
if oauth_jwt_legacy_public_key
|
647
|
-
algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
648
|
-
JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
|
649
|
-
elsif jwks
|
650
|
-
JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
|
651
|
-
elsif jws_key
|
652
|
-
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
|
653
|
-
end
|
654
|
-
elsif (jwks = auth_server_jwks_set)
|
655
|
-
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
656
|
-
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
|
657
|
-
end
|
658
|
-
|
659
|
-
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
|
660
|
-
|
661
|
-
claims
|
662
|
-
rescue JWT::DecodeError, JWT::JWKError
|
663
|
-
nil
|
664
|
-
end
|
665
|
-
|
666
|
-
if defined?(JWE)
|
667
|
-
def jwt_decode_with_jwe(
|
668
|
-
token,
|
669
|
-
jwks: nil,
|
670
|
-
jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
|
671
|
-
jws_encryption_method: oauth_jwt_jwe_encryption_method,
|
672
|
-
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
|
673
|
-
**args
|
674
|
-
)
|
675
|
-
|
676
|
-
token = if jwks && jwks.any? { |k| k[:use] == "enc" }
|
677
|
-
JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
|
678
|
-
elsif jwe_key
|
679
|
-
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
680
|
-
JWE.decrypt(token, jwe_key)
|
681
|
-
else
|
682
|
-
token
|
683
|
-
end
|
684
|
-
|
685
|
-
jwt_decode_without_jwe(token, jwks: jwks, **args)
|
686
|
-
rescue JWE::DecodeError => e
|
687
|
-
jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
|
688
|
-
end
|
689
|
-
|
690
|
-
alias_method :jwt_decode_without_jwe, :jwt_decode
|
691
|
-
alias_method :jwt_decode, :jwt_decode_with_jwe
|
692
|
-
end
|
693
|
-
|
694
|
-
def jwks_set
|
695
|
-
@jwks_set ||= [
|
696
|
-
*(
|
697
|
-
unless oauth_jwt_public_keys.empty?
|
698
|
-
oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JWT::JWK.new(pkey).export.merge(use: "sig", alg: algo) } }
|
699
|
-
end
|
700
|
-
),
|
701
|
-
*(
|
702
|
-
unless oauth_jwt_jwe_public_keys.empty?
|
703
|
-
oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
|
704
|
-
pkeys.map do |pkey|
|
705
|
-
JWT::JWK.new(pkey).export.merge(use: "enc", alg: algo)
|
706
|
-
end
|
707
|
-
end
|
708
|
-
end
|
709
|
-
),
|
710
|
-
# legacy
|
711
|
-
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
712
|
-
(
|
713
|
-
if oauth_jwt_legacy_public_key
|
714
|
-
JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
|
715
|
-
end
|
716
|
-
),
|
717
|
-
(JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
718
|
-
].compact
|
719
|
-
end
|
720
|
-
else
|
721
|
-
# :nocov:
|
722
|
-
def jwk_import(_data)
|
723
|
-
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
724
|
-
end
|
725
|
-
|
726
|
-
def jwt_encode(_token)
|
727
|
-
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
728
|
-
end
|
729
|
-
|
730
|
-
def jwt_decode(_token, **)
|
731
|
-
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
732
|
-
end
|
733
|
-
|
734
|
-
def jwks_set
|
735
|
-
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
736
|
-
end
|
737
|
-
# :nocov:
|
738
|
-
end
|
739
|
-
|
740
|
-
def validate_oauth_revoke_params
|
741
|
-
token_hint = param_or_nil("token_type_hint")
|
742
|
-
|
743
|
-
throw(:rodauth_error) if !token_hint || token_hint == "access_token"
|
744
|
-
|
745
|
-
super
|
746
|
-
end
|
747
|
-
|
748
|
-
def jwt_response_success(jwt, cache = false)
|
749
|
-
response.status = 200
|
750
|
-
response["Content-Type"] ||= "application/jwt"
|
751
|
-
if cache
|
752
|
-
# defaulting to 1-day for everyone, for now at least
|
753
|
-
max_age = 60 * 60 * 24
|
754
|
-
response["Cache-Control"] = "private, max-age=#{max_age}"
|
755
|
-
else
|
756
|
-
response["Cache-Control"] = "no-store"
|
757
|
-
response["Pragma"] = "no-cache"
|
758
|
-
end
|
759
|
-
return_response(jwt)
|
760
|
-
end
|
761
125
|
end
|
762
126
|
end
|