rodauth-oauth 1.1.0 → 1.3.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/README.md +11 -8
- data/doc/release_notes/1_1_0.md +1 -1
- data/doc/release_notes/1_2_0.md +36 -0
- data/doc/release_notes/1_3_0.md +38 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +32 -9
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +55 -33
- data/lib/rodauth/features/oauth_authorize_base.rb +25 -3
- data/lib/rodauth/features/oauth_base.rb +16 -16
- data/lib/rodauth/features/oauth_device_code_grant.rb +1 -2
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +182 -29
- data/lib/rodauth/features/oauth_implicit_grant.rb +23 -5
- data/lib/rodauth/features/oauth_jwt.rb +2 -0
- data/lib/rodauth/features/oauth_jwt_base.rb +52 -11
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +30 -22
- data/lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb +126 -0
- data/lib/rodauth/features/oauth_management_base.rb +1 -3
- data/lib/rodauth/features/oauth_pushed_authorization_request.rb +135 -0
- data/lib/rodauth/features/oauth_tls_client_auth.rb +170 -0
- data/lib/rodauth/features/oidc.rb +97 -59
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +52 -2
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +3 -4
- data/lib/rodauth/features/oidc_self_issued.rb +73 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +1 -0
- metadata +10 -2
@@ -9,16 +9,78 @@ module Rodauth
|
|
9
9
|
before "register"
|
10
10
|
|
11
11
|
auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
|
12
|
+
auth_value_method :oauth_applications_registration_access_token_column, :registration_access_token
|
13
|
+
auth_value_method :registration_client_uri_route, "register"
|
12
14
|
|
13
15
|
PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
|
14
16
|
|
17
|
+
def load_registration_client_uri_routes
|
18
|
+
request.on(registration_client_uri_route) do
|
19
|
+
# CLIENT REGISTRATION URI
|
20
|
+
request.on(String) do |client_id|
|
21
|
+
(token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
|
22
|
+
|
23
|
+
next unless token
|
24
|
+
|
25
|
+
oauth_application = db[oauth_applications_table]
|
26
|
+
.where(oauth_applications_client_id_column => client_id)
|
27
|
+
.first
|
28
|
+
next unless oauth_application
|
29
|
+
|
30
|
+
authorization_required unless password_hash_match?(oauth_application[oauth_applications_registration_access_token_column], token)
|
31
|
+
|
32
|
+
request.is do
|
33
|
+
request.get do
|
34
|
+
json_response_oauth_application(oauth_application)
|
35
|
+
end
|
36
|
+
request.on method: :put do
|
37
|
+
%w[client_id registration_access_token registration_client_uri client_secret_expires_at
|
38
|
+
client_id_issued_at].each do |prohibited_param|
|
39
|
+
if request.params.key?(prohibited_param)
|
40
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(prohibited_param))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
validate_client_registration_params
|
44
|
+
|
45
|
+
# if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
|
46
|
+
# issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
|
47
|
+
# its own chosen value.
|
48
|
+
authorization_required if request.params.key?("client_secret") && secret_matches?(oauth_application,
|
49
|
+
request.params["client_secret"])
|
50
|
+
|
51
|
+
oauth_application = transaction do
|
52
|
+
applications_ds = db[oauth_applications_table]
|
53
|
+
__update_and_return__(applications_ds, @oauth_application_params)
|
54
|
+
end
|
55
|
+
json_response_oauth_application(oauth_application)
|
56
|
+
end
|
57
|
+
|
58
|
+
request.on method: :delete do
|
59
|
+
applications_ds = db[oauth_applications_table]
|
60
|
+
applications_ds.where(oauth_applications_client_id_column => client_id).delete
|
61
|
+
response.status = 204
|
62
|
+
response["Cache-Control"] = "no-store"
|
63
|
+
response["Pragma"] = "no-cache"
|
64
|
+
response.finish
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
15
71
|
# /register
|
16
72
|
auth_server_route(:register) do |r|
|
17
73
|
before_register_route
|
18
74
|
|
19
|
-
validate_client_registration_params
|
20
|
-
|
21
75
|
r.post do
|
76
|
+
oauth_client_registration_required_params.each do |required_param|
|
77
|
+
unless request.params.key?(required_param)
|
78
|
+
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
validate_client_registration_params
|
83
|
+
|
22
84
|
response_params = transaction do
|
23
85
|
before_register
|
24
86
|
do_register
|
@@ -56,14 +118,8 @@ module Rodauth
|
|
56
118
|
}
|
57
119
|
end
|
58
120
|
|
59
|
-
def validate_client_registration_params
|
60
|
-
|
61
|
-
unless request.params.key?(required_param)
|
62
|
-
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
@oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
|
121
|
+
def validate_client_registration_params(request_params = request.params)
|
122
|
+
@oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
|
67
123
|
case key
|
68
124
|
when "redirect_uris"
|
69
125
|
if value.is_a?(Array)
|
@@ -96,7 +152,7 @@ module Rodauth
|
|
96
152
|
key = oauth_applications_grant_types_column
|
97
153
|
when "response_types"
|
98
154
|
if value.is_a?(Array)
|
99
|
-
grant_types =
|
155
|
+
grant_types = request_params["grant_types"] || %w[authorization_code]
|
100
156
|
value = value.each do |response_type|
|
101
157
|
unless oauth_response_types_supported.include?(response_type)
|
102
158
|
register_throw_json_response_error("invalid_client_metadata",
|
@@ -114,9 +170,9 @@ module Rodauth
|
|
114
170
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
|
115
171
|
case key
|
116
172
|
when "client_uri"
|
117
|
-
key =
|
173
|
+
key = oauth_applications_homepage_url_column
|
118
174
|
when "jwks_uri"
|
119
|
-
if
|
175
|
+
if request_params.key?("jwks")
|
120
176
|
register_throw_json_response_error("invalid_client_metadata",
|
121
177
|
register_invalid_jwks_param_message(key, "jwks"))
|
122
178
|
end
|
@@ -124,7 +180,7 @@ module Rodauth
|
|
124
180
|
key = __send__(:"oauth_applications_#{key}_column")
|
125
181
|
when "jwks"
|
126
182
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
|
127
|
-
if
|
183
|
+
if request_params.key?("jwks_uri")
|
128
184
|
register_throw_json_response_error("invalid_client_metadata",
|
129
185
|
register_invalid_jwks_param_message(key, "jwks_uri"))
|
130
186
|
end
|
@@ -132,6 +188,7 @@ module Rodauth
|
|
132
188
|
key = oauth_applications_jwks_column
|
133
189
|
value = JSON.dump(value)
|
134
190
|
when "scope"
|
191
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
|
135
192
|
scopes = value.split(" ") - oauth_application_scopes
|
136
193
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
|
137
194
|
key = oauth_applications_scopes_column
|
@@ -141,7 +198,37 @@ module Rodauth
|
|
141
198
|
value = value.join(" ")
|
142
199
|
key = oauth_applications_contacts_column
|
143
200
|
when "client_name"
|
201
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
|
144
202
|
key = oauth_applications_name_column
|
203
|
+
when "require_pushed_authorization_requests"
|
204
|
+
unless respond_to?(:oauth_applications_require_pushed_authorization_requests_column)
|
205
|
+
register_throw_json_response_error("invalid_client_metadata",
|
206
|
+
register_invalid_param_message(key))
|
207
|
+
end
|
208
|
+
request_params[key] = value = convert_to_boolean(key, value)
|
209
|
+
|
210
|
+
key = oauth_applications_require_pushed_authorization_requests_column
|
211
|
+
when "tls_client_certificate_bound_access_tokens"
|
212
|
+
property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
|
213
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
|
214
|
+
|
215
|
+
request_params[key] = value = convert_to_boolean(key, value)
|
216
|
+
|
217
|
+
key = oauth_applications_tls_client_certificate_bound_access_tokens_column
|
218
|
+
when /\Atls_client_auth_/
|
219
|
+
unless respond_to?(:"oauth_applications_#{key}_column")
|
220
|
+
register_throw_json_response_error("invalid_client_metadata",
|
221
|
+
register_invalid_param_message(key))
|
222
|
+
end
|
223
|
+
|
224
|
+
# client using the tls_client_auth authentication method MUST use exactly one of the below metadata
|
225
|
+
# parameters to indicate the certificate subject value that the authorization server is to expect when
|
226
|
+
# authenticating the respective client.
|
227
|
+
if params.any? { |k, _| k.to_s.start_with?("tls_client_auth_") }
|
228
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
229
|
+
end
|
230
|
+
|
231
|
+
key = __send__(:"oauth_applications_#{key}_column")
|
145
232
|
else
|
146
233
|
if respond_to?(:"oauth_applications_#{key}_column")
|
147
234
|
if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
|
@@ -183,42 +270,60 @@ module Rodauth
|
|
183
270
|
|
184
271
|
# set defaults
|
185
272
|
create_params = @oauth_application_params
|
273
|
+
|
274
|
+
# If omitted, an authorization server MAY register a client with a default set of scopes
|
186
275
|
create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
|
276
|
+
|
277
|
+
# https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
187
278
|
if create_params[oauth_applications_grant_types_column] ||= begin
|
279
|
+
# If omitted, the default behavior is that the client will use only the "authorization_code" Grant Type.
|
188
280
|
return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
|
189
281
|
"authorization_code"
|
190
282
|
end
|
191
283
|
create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
|
284
|
+
# If unspecified or omitted, the default is "client_secret_basic", denoting the HTTP Basic
|
285
|
+
# authentication scheme as specified in Section 2.3.1 of OAuth 2.0.
|
192
286
|
return_params["token_endpoint_auth_method"] = "client_secret_basic"
|
193
287
|
"client_secret_basic"
|
194
288
|
end
|
195
289
|
end
|
196
290
|
create_params[oauth_applications_response_types_column] ||= begin
|
291
|
+
# If omitted, the default is that the client will use only the "code" response type.
|
197
292
|
return_params["response_types"] = %w[code]
|
198
293
|
"code"
|
199
294
|
end
|
200
295
|
rescue_from_uniqueness_error do
|
201
|
-
|
202
|
-
create_params
|
203
|
-
return_params["client_id"] = client_id
|
204
|
-
return_params["client_id_issued_at"] = Time.now.utc.iso8601
|
205
|
-
if create_params.key?(oauth_applications_client_secret_column)
|
206
|
-
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
|
207
|
-
return_params.delete("client_secret")
|
208
|
-
else
|
209
|
-
client_secret = oauth_unique_id_generator
|
210
|
-
set_client_secret(create_params, client_secret)
|
211
|
-
return_params["client_secret"] = client_secret
|
212
|
-
return_params["client_secret_expires_at"] = 0
|
213
|
-
|
214
|
-
create_params.delete_if { |k, _| !application_columns.include?(k) }
|
215
|
-
end
|
296
|
+
initialize_register_params(create_params, return_params)
|
297
|
+
create_params.delete_if { |k, _| !application_columns.include?(k) }
|
216
298
|
applications_ds.insert(create_params)
|
217
299
|
end
|
218
300
|
|
219
301
|
return_params
|
220
302
|
end
|
221
303
|
|
304
|
+
def initialize_register_params(create_params, return_params)
|
305
|
+
client_id = oauth_unique_id_generator
|
306
|
+
create_params[oauth_applications_client_id_column] = client_id
|
307
|
+
return_params["client_id"] = client_id
|
308
|
+
return_params["client_id_issued_at"] = Time.now.utc.iso8601
|
309
|
+
|
310
|
+
registration_access_token = oauth_unique_id_generator
|
311
|
+
create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
|
312
|
+
return_params["registration_access_token"] = registration_access_token
|
313
|
+
return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
|
314
|
+
|
315
|
+
if create_params.key?(oauth_applications_client_secret_column)
|
316
|
+
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
|
317
|
+
return_params.delete("client_secret")
|
318
|
+
else
|
319
|
+
client_secret = oauth_unique_id_generator
|
320
|
+
set_client_secret(create_params, client_secret)
|
321
|
+
return_params["client_secret"] = client_secret
|
322
|
+
return_params["client_secret_expires_at"] = 0
|
323
|
+
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
222
327
|
def register_throw_json_response_error(code, message)
|
223
328
|
throw_json_response_error(oauth_invalid_response_status, code, message)
|
224
329
|
end
|
@@ -264,6 +369,54 @@ module Rodauth
|
|
264
369
|
"type '#{response_type}' to be allowed."
|
265
370
|
end
|
266
371
|
|
372
|
+
def convert_to_boolean(key, value)
|
373
|
+
case value
|
374
|
+
when "true" then true
|
375
|
+
when "false" then false
|
376
|
+
else
|
377
|
+
register_throw_json_response_error(
|
378
|
+
"invalid_client_metadata",
|
379
|
+
register_invalid_param_message(key)
|
380
|
+
)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def json_response_oauth_application(oauth_application)
|
385
|
+
params = methods.map { |k| k.to_s[/\Aoauth_applications_(\w+)_column\z/, 1] }.compact
|
386
|
+
|
387
|
+
body = params.each_with_object({}) do |k, hash|
|
388
|
+
next if %w[id account_id client_id client_secret cliennt_secret_hash].include?(k)
|
389
|
+
|
390
|
+
value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
|
391
|
+
|
392
|
+
next unless value
|
393
|
+
|
394
|
+
case k
|
395
|
+
when "redirect_uri"
|
396
|
+
hash["redirect_uris"] = value.split(" ")
|
397
|
+
when "token_endpoint_auth_method", "grant_types", "response_types", "request_uris", "post_logout_redirect_uris"
|
398
|
+
hash[k] = value.split(" ")
|
399
|
+
when "scopes"
|
400
|
+
hash["scope"] = value
|
401
|
+
when "jwks"
|
402
|
+
hash[k] = value.is_a?(String) ? JSON.parse(value) : value
|
403
|
+
when "homepage_url"
|
404
|
+
hash["client_uri"] = value
|
405
|
+
when "name"
|
406
|
+
hash["client_name"] = value
|
407
|
+
else
|
408
|
+
hash[k] = value
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
response.status = 200
|
413
|
+
response["Content-Type"] ||= json_response_content_type
|
414
|
+
response["Cache-Control"] = "no-store"
|
415
|
+
response["Pragma"] = "no-cache"
|
416
|
+
json_payload = _json_response_body(body)
|
417
|
+
return_response(json_payload)
|
418
|
+
end
|
419
|
+
|
267
420
|
def oauth_server_metadata_body(*)
|
268
421
|
super.tap do |data|
|
269
422
|
data[:registration_endpoint] = register_url
|
@@ -20,6 +20,24 @@ module Rodauth
|
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
+
def validate_authorize_params
|
24
|
+
super
|
25
|
+
|
26
|
+
response_mode = param_or_nil("response_mode")
|
27
|
+
|
28
|
+
return unless response_mode
|
29
|
+
|
30
|
+
response_type = param_or_nil("response_type")
|
31
|
+
|
32
|
+
return unless response_type == "token"
|
33
|
+
|
34
|
+
redirect_response_error("invalid_request") unless oauth_response_modes_for_token_supported.include?(response_mode)
|
35
|
+
end
|
36
|
+
|
37
|
+
def oauth_response_modes_for_token_supported
|
38
|
+
%w[fragment]
|
39
|
+
end
|
40
|
+
|
23
41
|
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
24
42
|
response_type = param("response_type")
|
25
43
|
return super unless response_type == "token" && supported_response_type?(response_type)
|
@@ -42,19 +60,19 @@ module Rodauth
|
|
42
60
|
oauth_grants_type_column => "implicit",
|
43
61
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
44
62
|
oauth_grants_scopes_column => scopes,
|
45
|
-
|
63
|
+
**resource_owner_params
|
46
64
|
}.merge(grant_params)
|
47
65
|
|
48
66
|
generate_token(grant_params, false)
|
49
67
|
end
|
50
68
|
|
51
|
-
def _redirect_response_error(redirect_url,
|
69
|
+
def _redirect_response_error(redirect_url, params)
|
52
70
|
response_types = param("response_type").split(/ +/)
|
53
71
|
|
54
72
|
return super if response_types.empty? || response_types == %w[code]
|
55
73
|
|
56
|
-
|
57
|
-
redirect_url.fragment =
|
74
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
75
|
+
redirect_url.fragment = params.join("&")
|
58
76
|
redirect(redirect_url.to_s)
|
59
77
|
end
|
60
78
|
|
@@ -62,7 +80,7 @@ module Rodauth
|
|
62
80
|
return super unless mode == "fragment"
|
63
81
|
|
64
82
|
redirect_url = URI.parse(redirect_uri)
|
65
|
-
params = params
|
83
|
+
params = [URI.encode_www_form(params)]
|
66
84
|
params << redirect_url.query if redirect_url.query
|
67
85
|
redirect_url.fragment = params.join("&")
|
68
86
|
redirect(redirect_url.to_s)
|
@@ -24,7 +24,8 @@ module Rodauth
|
|
24
24
|
:jwt_decode_no_key,
|
25
25
|
:generate_jti,
|
26
26
|
:oauth_jwt_issuer,
|
27
|
-
:oauth_jwt_audience
|
27
|
+
:oauth_jwt_audience,
|
28
|
+
:resource_owner_params_from_jwt_claims
|
28
29
|
)
|
29
30
|
|
30
31
|
private
|
@@ -70,6 +71,10 @@ module Rodauth
|
|
70
71
|
client_application[oauth_applications_client_id_column]
|
71
72
|
end
|
72
73
|
|
74
|
+
def resource_owner_params_from_jwt_claims(claims)
|
75
|
+
{ oauth_grants_account_id_column => claims["sub"] }
|
76
|
+
end
|
77
|
+
|
73
78
|
def oauth_server_metadata_body(path = nil)
|
74
79
|
metadata = super
|
75
80
|
metadata.merge! \
|
@@ -81,14 +86,6 @@ module Rodauth
|
|
81
86
|
@_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
|
82
87
|
end
|
83
88
|
|
84
|
-
def _jwt_public_key
|
85
|
-
@_jwt_public_key ||= if oauth_application
|
86
|
-
oauth_application_jwks(oauth_application)
|
87
|
-
else
|
88
|
-
_jwt_key
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
89
|
# Resource Server only!
|
93
90
|
#
|
94
91
|
# returns the jwks set from the authorization server.
|
@@ -152,10 +149,28 @@ module Rodauth
|
|
152
149
|
A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
|
153
150
|
]
|
154
151
|
|
155
|
-
def
|
152
|
+
def key_to_jwk(key)
|
156
153
|
JSON::JWK.new(key)
|
157
154
|
end
|
158
155
|
|
156
|
+
def jwk_export(key)
|
157
|
+
key_to_jwk(key)
|
158
|
+
end
|
159
|
+
|
160
|
+
def jwk_import(jwk)
|
161
|
+
JSON::JWK.new(jwk)
|
162
|
+
end
|
163
|
+
|
164
|
+
def jwk_key(jwk)
|
165
|
+
jwk = jwk_import(jwk) unless jwk.is_a?(JSON::JWK)
|
166
|
+
jwk.to_key
|
167
|
+
end
|
168
|
+
|
169
|
+
def jwk_thumbprint(jwk)
|
170
|
+
jwk = jwk_import(jwk) if jwk.is_a?(Hash)
|
171
|
+
jwk.thumbprint
|
172
|
+
end
|
173
|
+
|
159
174
|
def jwt_encode(payload,
|
160
175
|
jwks: nil,
|
161
176
|
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
|
@@ -287,8 +302,26 @@ module Rodauth
|
|
287
302
|
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
|
288
303
|
end
|
289
304
|
|
305
|
+
def key_to_jwk(key)
|
306
|
+
JWT::JWK.new(key)
|
307
|
+
end
|
308
|
+
|
290
309
|
def jwk_export(key)
|
291
|
-
|
310
|
+
key_to_jwk(key).export
|
311
|
+
end
|
312
|
+
|
313
|
+
def jwk_import(jwk)
|
314
|
+
JWT::JWK.import(jwk)
|
315
|
+
end
|
316
|
+
|
317
|
+
def jwk_key(jwk)
|
318
|
+
jwk = jwk_import(jwk) unless jwk.is_a?(JWT::JWK)
|
319
|
+
jwk.keypair
|
320
|
+
end
|
321
|
+
|
322
|
+
def jwk_thumbprint(jwk)
|
323
|
+
jwk = jwk_import(jwk) if jwk.is_a?(Hash)
|
324
|
+
JWT::JWK::Thumbprint.new(jwk).generate
|
292
325
|
end
|
293
326
|
|
294
327
|
def jwt_encode(payload,
|
@@ -445,6 +478,14 @@ module Rodauth
|
|
445
478
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
446
479
|
end
|
447
480
|
|
481
|
+
def jwk_import(_jwk)
|
482
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
483
|
+
end
|
484
|
+
|
485
|
+
def jwk_thumbprint(_jwk)
|
486
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
487
|
+
end
|
488
|
+
|
448
489
|
def jwt_encode(_token)
|
449
490
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
450
491
|
end
|
@@ -46,28 +46,7 @@ module Rodauth
|
|
46
46
|
request_object = response.body
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
|
-
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
51
|
-
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
52
|
-
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
53
|
-
}.compact
|
54
|
-
|
55
|
-
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
|
56
|
-
|
57
|
-
if request_sig_enc_opts[:jws_algorithm] == "none"
|
58
|
-
jwks = nil
|
59
|
-
elsif (jwks = oauth_application_jwks(oauth_application))
|
60
|
-
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
61
|
-
else
|
62
|
-
redirect_response_error("invalid_request_object")
|
63
|
-
end
|
64
|
-
|
65
|
-
claims = jwt_decode(request_object,
|
66
|
-
jwks: jwks,
|
67
|
-
verify_jti: false,
|
68
|
-
verify_iss: false,
|
69
|
-
verify_aud: false,
|
70
|
-
**request_sig_enc_opts)
|
49
|
+
claims = decode_request_object(request_object)
|
71
50
|
|
72
51
|
redirect_response_error("invalid_request_object") unless claims
|
73
52
|
|
@@ -105,6 +84,35 @@ module Rodauth
|
|
105
84
|
request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
|
106
85
|
end
|
107
86
|
|
87
|
+
def decode_request_object(request_object)
|
88
|
+
request_sig_enc_opts = {
|
89
|
+
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
90
|
+
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
91
|
+
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
92
|
+
}.compact
|
93
|
+
|
94
|
+
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
|
95
|
+
|
96
|
+
if request_sig_enc_opts[:jws_algorithm] == "none"
|
97
|
+
jwks = nil
|
98
|
+
elsif (jwks = oauth_application_jwks(oauth_application))
|
99
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
100
|
+
else
|
101
|
+
redirect_response_error("invalid_request_object")
|
102
|
+
end
|
103
|
+
|
104
|
+
claims = jwt_decode(request_object,
|
105
|
+
jwks: jwks,
|
106
|
+
verify_jti: false,
|
107
|
+
verify_iss: false,
|
108
|
+
verify_aud: false,
|
109
|
+
**request_sig_enc_opts)
|
110
|
+
|
111
|
+
redirect_response_error("invalid_request_object") unless claims
|
112
|
+
|
113
|
+
claims
|
114
|
+
end
|
115
|
+
|
108
116
|
def oauth_server_metadata_body(*)
|
109
117
|
super.tap do |data|
|
110
118
|
data[:request_parameter_supported] = true
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt_secured_authorization_response_mode, :OauthJwtSecuredAuthorizationResponseMode) do
|
7
|
+
depends :oauth_authorize_base, :oauth_jwt_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_authorization_response_mode_expires_in, 60 * 5 # 5 minutes
|
10
|
+
|
11
|
+
auth_value_method :oauth_applications_authorization_signed_response_alg_column, :authorization_signed_response_alg
|
12
|
+
auth_value_method :oauth_applications_authorization_encrypted_response_alg_column, :authorization_encrypted_response_alg
|
13
|
+
auth_value_method :oauth_applications_authorization_encrypted_response_enc_column, :authorization_encrypted_response_enc
|
14
|
+
|
15
|
+
auth_value_methods(
|
16
|
+
:authorization_signing_alg_values_supported,
|
17
|
+
:authorization_encryption_alg_values_supported,
|
18
|
+
:authorization_encryption_enc_values_supported
|
19
|
+
)
|
20
|
+
|
21
|
+
def oauth_response_modes_supported
|
22
|
+
jwt_response_modes = %w[jwt]
|
23
|
+
jwt_response_modes.push("query.jwt", "form_post.jwt") if features.include?(:oauth_authorization_code_grant)
|
24
|
+
jwt_response_modes << "fragment.jwt" if features.include?(:oauth_implicit_grant)
|
25
|
+
|
26
|
+
super | jwt_response_modes
|
27
|
+
end
|
28
|
+
|
29
|
+
def authorization_signing_alg_values_supported
|
30
|
+
oauth_jwt_jws_algorithms_supported
|
31
|
+
end
|
32
|
+
|
33
|
+
def authorization_encryption_alg_values_supported
|
34
|
+
oauth_jwt_jwe_algorithms_supported
|
35
|
+
end
|
36
|
+
|
37
|
+
def authorization_encryption_enc_values_supported
|
38
|
+
oauth_jwt_jwe_encryption_methods_supported
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def oauth_response_modes_for_code_supported
|
44
|
+
return [] unless features.include?(:oauth_authorization_code_grant)
|
45
|
+
|
46
|
+
super | %w[query.jwt form_post.jwt jwt]
|
47
|
+
end
|
48
|
+
|
49
|
+
def oauth_response_modes_for_token_supported
|
50
|
+
return [] unless features.include?(:oauth_implicit_grant)
|
51
|
+
|
52
|
+
super | %w[fragment.jwt jwt]
|
53
|
+
end
|
54
|
+
|
55
|
+
def authorize_response(params, mode)
|
56
|
+
return super unless mode.end_with?("jwt")
|
57
|
+
|
58
|
+
response_type = param_or_nil("response_type")
|
59
|
+
|
60
|
+
redirect_url = URI.parse(redirect_uri)
|
61
|
+
|
62
|
+
jwt = jwt_encode_authorization_response_mode(params)
|
63
|
+
|
64
|
+
if mode == "query.jwt" || (mode == "jwt" && response_type == "code")
|
65
|
+
return super unless features.include?(:oauth_authorization_code_grant)
|
66
|
+
|
67
|
+
params = ["response=#{CGI.escape(jwt)}"]
|
68
|
+
params << redirect_url.query if redirect_url.query
|
69
|
+
redirect_url.query = params.join("&")
|
70
|
+
redirect(redirect_url.to_s)
|
71
|
+
elsif mode == "form_post.jwt"
|
72
|
+
return super unless features.include?(:oauth_authorization_code_grant)
|
73
|
+
|
74
|
+
response["Content-Type"] = "text/html"
|
75
|
+
body = form_post_response_html(redirect_url) do
|
76
|
+
"<input type=\"hidden\" name=\"response\" value=\"#{scope.h(jwt)}\" />"
|
77
|
+
end
|
78
|
+
response.write(body)
|
79
|
+
request.halt
|
80
|
+
elsif mode == "fragment.jwt" || (mode == "jwt" && response_type == "token")
|
81
|
+
return super unless features.include?(:oauth_implicit_grant)
|
82
|
+
|
83
|
+
params = ["response=#{CGI.escape(jwt)}"]
|
84
|
+
params << redirect_url.query if redirect_url.query
|
85
|
+
redirect_url.fragment = params.join("&")
|
86
|
+
redirect(redirect_url.to_s)
|
87
|
+
else
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def _redirect_response_error(redirect_url, params)
|
93
|
+
response_mode = param_or_nil("response_mode")
|
94
|
+
return super unless response_mode.end_with?("jwt")
|
95
|
+
|
96
|
+
authorize_response(Hash[params], response_mode)
|
97
|
+
end
|
98
|
+
|
99
|
+
def jwt_encode_authorization_response_mode(params)
|
100
|
+
now = Time.now.to_i
|
101
|
+
claims = {
|
102
|
+
iss: oauth_jwt_issuer,
|
103
|
+
aud: oauth_application[oauth_applications_client_id_column],
|
104
|
+
exp: now + oauth_authorization_response_mode_expires_in,
|
105
|
+
iat: now
|
106
|
+
}.merge(params)
|
107
|
+
|
108
|
+
encode_params = {
|
109
|
+
jwks: oauth_application_jwks(oauth_application),
|
110
|
+
signing_algorithm: oauth_application[oauth_applications_authorization_signed_response_alg_column],
|
111
|
+
encryption_algorithm: oauth_application[oauth_applications_authorization_encrypted_response_alg_column],
|
112
|
+
encryption_method: oauth_application[oauth_applications_authorization_encrypted_response_enc_column]
|
113
|
+
}.compact
|
114
|
+
|
115
|
+
jwt_encode(claims, **encode_params)
|
116
|
+
end
|
117
|
+
|
118
|
+
def oauth_server_metadata_body(*)
|
119
|
+
super.tap do |data|
|
120
|
+
data[:authorization_signing_alg_values_supported] = authorization_signing_alg_values_supported
|
121
|
+
data[:authorization_encryption_alg_values_supported] = authorization_encryption_alg_values_supported
|
122
|
+
data[:authorization_encryption_enc_values_supported] = authorization_encryption_enc_values_supported
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -23,9 +23,7 @@ module Rodauth
|
|
23
23
|
classes += " disabled" if current || !page
|
24
24
|
classes += " active" if current
|
25
25
|
if page
|
26
|
-
params = request.GET.merge("page" => page)
|
27
|
-
v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
|
28
|
-
end.join("&")
|
26
|
+
params = URI.encode_www_form(request.GET.merge("page" => page))
|
29
27
|
|
30
28
|
href = "#{request.path}?#{params}"
|
31
29
|
|