rodauth-oauth 1.1.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|