rodauth-oauth 0.8.0 → 0.9.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 +5 -2
- data/doc/release_notes/0_9_0.md +56 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +22 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +8 -3
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +8 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +1 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +1 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +13 -1
- data/lib/rodauth/features/oauth.rb +2 -2
- data/lib/rodauth/features/oauth_application_management.rb +22 -6
- data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +4 -1
- data/lib/rodauth/features/oauth_base.rb +46 -10
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
- data/lib/rodauth/features/oauth_device_grant.rb +4 -5
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
- data/lib/rodauth/features/oauth_jwt.rb +248 -49
- data/lib/rodauth/features/oauth_management_base.rb +68 -0
- data/lib/rodauth/features/oauth_pkce.rb +1 -1
- data/lib/rodauth/features/oauth_token_management.rb +8 -6
- data/lib/rodauth/features/oidc.rb +32 -3
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
- data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
- data/lib/rodauth/oauth/ttl_store.rb +9 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +5 -0
- data/templates/authorize.str +50 -1
- data/templates/jwks_field.str +4 -0
- data/templates/oauth_application.str +1 -1
- data/templates/oauth_application_oauth_tokens.str +1 -0
- data/templates/oauth_applications.str +1 -0
- data/templates/oauth_tokens.str +1 -0
- metadata +10 -3
- data/templates/jws_jwk_field.str +0 -4
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_dynamic_client_registration, :OauthDynamicClientRegistration) do
|
5
|
+
depends :oauth_base
|
6
|
+
|
7
|
+
before "register"
|
8
|
+
|
9
|
+
auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name client_uri]
|
10
|
+
|
11
|
+
PROTECTED_APPLICATION_ATTRIBUTES = %i[account_id client_id].freeze
|
12
|
+
|
13
|
+
# /register
|
14
|
+
route(:register) do |r|
|
15
|
+
next unless is_authorization_server?
|
16
|
+
|
17
|
+
before_register_route
|
18
|
+
|
19
|
+
validate_client_registration_params
|
20
|
+
|
21
|
+
r.post do
|
22
|
+
response_params = transaction do
|
23
|
+
before_register
|
24
|
+
do_register
|
25
|
+
end
|
26
|
+
|
27
|
+
response.status = 201
|
28
|
+
response["Content-Type"] = json_response_content_type
|
29
|
+
response["Cache-Control"] = "no-store"
|
30
|
+
response["Pragma"] = "no-cache"
|
31
|
+
response.write(_json_response_body(response_params))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_csrf?
|
36
|
+
case request.path
|
37
|
+
when register_path
|
38
|
+
false
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def registration_metadata
|
47
|
+
oauth_server_metadata_body
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_client_registration_params
|
51
|
+
oauth_client_registration_required_params.each do |required_param|
|
52
|
+
unless request.params.key?(required_param)
|
53
|
+
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
metadata = registration_metadata
|
57
|
+
|
58
|
+
@oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
|
59
|
+
case key
|
60
|
+
when "redirect_uris"
|
61
|
+
if value.is_a?(Array)
|
62
|
+
value = value.each do |uri|
|
63
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri)) unless check_valid_uri?(uri)
|
64
|
+
end.join(" ")
|
65
|
+
else
|
66
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
|
67
|
+
end
|
68
|
+
key = oauth_applications_redirect_uri_column
|
69
|
+
when "token_endpoint_auth_method"
|
70
|
+
unless oauth_auth_methods_supported.include?(value)
|
71
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
72
|
+
end
|
73
|
+
# verify if in range
|
74
|
+
key = oauth_applications_token_endpoint_auth_method_column
|
75
|
+
when "grant_types"
|
76
|
+
if value.is_a?(Array)
|
77
|
+
value = value.each do |grant_type|
|
78
|
+
unless metadata[:grant_types_supported].include?(grant_type)
|
79
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_grant_type_message(grant_type))
|
80
|
+
end
|
81
|
+
end.join(" ")
|
82
|
+
else
|
83
|
+
set_field_error(key, invalid_client_metadata_message)
|
84
|
+
end
|
85
|
+
key = oauth_applications_grant_types_column
|
86
|
+
when "response_types"
|
87
|
+
if value.is_a?(Array)
|
88
|
+
grant_types = request.params["grant_types"] || metadata[:grant_types_supported]
|
89
|
+
value = value.each do |response_type|
|
90
|
+
unless metadata[:response_types_supported].include?(response_type)
|
91
|
+
register_throw_json_response_error("invalid_client_metadata",
|
92
|
+
register_invalid_response_type_message(response_type))
|
93
|
+
end
|
94
|
+
|
95
|
+
validate_client_registration_response_type(response_type, grant_types)
|
96
|
+
end.join(" ")
|
97
|
+
else
|
98
|
+
set_field_error(key, invalid_client_metadata_message)
|
99
|
+
end
|
100
|
+
key = oauth_applications_response_types_column
|
101
|
+
# verify if in range and match grant type
|
102
|
+
when "client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"
|
103
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
|
104
|
+
case key
|
105
|
+
when "client_uri"
|
106
|
+
key = "homepage_url"
|
107
|
+
when "jwks_uri"
|
108
|
+
if request.params.key?("jwks")
|
109
|
+
register_throw_json_response_error("invalid_client_metadata",
|
110
|
+
register_invalid_jwks_param_message(key, "jwks"))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
key = __send__(:"oauth_applications_#{key}_column")
|
114
|
+
when "jwks"
|
115
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
|
116
|
+
if request.params.key?("jwks_uri")
|
117
|
+
register_throw_json_response_error("invalid_client_metadata",
|
118
|
+
register_invalid_jwks_param_message(key, "jwks_uri"))
|
119
|
+
end
|
120
|
+
|
121
|
+
key = oauth_applications_jwks_column
|
122
|
+
value = JSON.dump(value)
|
123
|
+
when "scope"
|
124
|
+
scopes = value.split(" ") - oauth_application_scopes
|
125
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
|
126
|
+
key = oauth_applications_scopes_column
|
127
|
+
# verify if in range
|
128
|
+
when "contacts"
|
129
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array)
|
130
|
+
value = value.join(" ")
|
131
|
+
key = oauth_applications_contacts_column
|
132
|
+
when "client_name"
|
133
|
+
key = oauth_applications_name_column
|
134
|
+
else
|
135
|
+
if respond_to?(:"oauth_applications_#{key}_column")
|
136
|
+
property = :"oauth_applications_#{key}_column"
|
137
|
+
if PROTECTED_APPLICATION_ATTRIBUTES.include?(property)
|
138
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
139
|
+
end
|
140
|
+
key = __send__(property)
|
141
|
+
elsif !db[oauth_applications_table].columns.include?(key.to_sym)
|
142
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
params[key] = value
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_client_registration_response_type(response_type, grant_types)
|
150
|
+
case response_type
|
151
|
+
when "code"
|
152
|
+
unless grant_types.include?("authorization_code")
|
153
|
+
register_throw_json_response_error("invalid_client_metadata",
|
154
|
+
register_invalid_response_type_for_grant_type_message(response_type,
|
155
|
+
"authorization_code"))
|
156
|
+
end
|
157
|
+
when "token"
|
158
|
+
unless grant_types.include?("implicit")
|
159
|
+
register_throw_json_response_error("invalid_client_metadata",
|
160
|
+
register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
|
161
|
+
end
|
162
|
+
when "none"
|
163
|
+
if grant_types.include?("implicit") || grant_types.include?("authorization_code")
|
164
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def do_register(return_params = request.params.dup)
|
170
|
+
# set defaults
|
171
|
+
create_params = @oauth_application_params
|
172
|
+
create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_default_scope.join(" ")
|
173
|
+
create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
|
174
|
+
return_params["token_endpoint_auth_method"] = "client_secret_basic"
|
175
|
+
"client_secret_basic"
|
176
|
+
end
|
177
|
+
create_params[oauth_applications_grant_types_column] ||= begin
|
178
|
+
return_params["grant_types"] = %w[authorization_code]
|
179
|
+
"authorization_code"
|
180
|
+
end
|
181
|
+
create_params[oauth_applications_response_types_column] ||= begin
|
182
|
+
return_params["response_types"] = %w[code]
|
183
|
+
"code"
|
184
|
+
end
|
185
|
+
rescue_from_uniqueness_error do
|
186
|
+
client_id = oauth_unique_id_generator
|
187
|
+
create_params[oauth_applications_client_id_column] = client_id
|
188
|
+
return_params["client_id"] = client_id
|
189
|
+
return_params["client_id_issued_at"] = Time.now.utc.iso8601
|
190
|
+
if create_params.key?(oauth_applications_client_secret_column)
|
191
|
+
create_params[oauth_applications_client_secret_column] = secret_hash(create_params[oauth_applications_client_secret_column])
|
192
|
+
return_params.delete("client_secret")
|
193
|
+
else
|
194
|
+
client_secret = oauth_unique_id_generator
|
195
|
+
create_params[oauth_applications_client_secret_column] = secret_hash(client_secret)
|
196
|
+
return_params["client_secret"] = client_secret
|
197
|
+
return_params["client_secret_expires_at"] = 0
|
198
|
+
end
|
199
|
+
db[oauth_applications_table].insert(create_params)
|
200
|
+
end
|
201
|
+
|
202
|
+
return_params
|
203
|
+
end
|
204
|
+
|
205
|
+
def register_throw_json_response_error(code, message)
|
206
|
+
throw_json_response_error(invalid_oauth_response_status, code, message)
|
207
|
+
end
|
208
|
+
|
209
|
+
def register_required_param_message(key)
|
210
|
+
"The param '#{key}' is required by this server."
|
211
|
+
end
|
212
|
+
|
213
|
+
def register_invalid_param_message(key)
|
214
|
+
"The param '#{key}' is not supported by this server."
|
215
|
+
end
|
216
|
+
|
217
|
+
def register_invalid_contacts_message(contacts)
|
218
|
+
"The contacts '#{contacts}' are not allowed by this server."
|
219
|
+
end
|
220
|
+
|
221
|
+
def register_invalid_uri_message(uri)
|
222
|
+
"The '#{uri}' URL is not allowed by this server."
|
223
|
+
end
|
224
|
+
|
225
|
+
def register_invalid_jwks_param_message(key1, key2)
|
226
|
+
"The param '#{key1}' cannot be accepted together with param '#{key2}'."
|
227
|
+
end
|
228
|
+
|
229
|
+
def register_invalid_scopes_message(scopes)
|
230
|
+
"The given scopes (#{scopes}) are not allowed by this server."
|
231
|
+
end
|
232
|
+
|
233
|
+
def register_invalid_grant_type_message(grant_type)
|
234
|
+
"The grant type #{grant_type} is not allowed by this server."
|
235
|
+
end
|
236
|
+
|
237
|
+
def register_invalid_response_type_message(response_type)
|
238
|
+
"The response type #{response_type} is not allowed by this server."
|
239
|
+
end
|
240
|
+
|
241
|
+
def register_invalid_response_type_for_grant_type_message(response_type, grant_type)
|
242
|
+
"The grant type '#{grant_type}' must be registered for the response " \
|
243
|
+
"type '#{response_type}' to be allowed."
|
244
|
+
end
|
245
|
+
|
246
|
+
def oauth_server_metadata_body(*)
|
247
|
+
super.tap do |data|
|
248
|
+
data[:registration_endpoint] = register_url
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -10,22 +10,38 @@ module Rodauth
|
|
10
10
|
|
11
11
|
# Recommended to have hmac_secret as well
|
12
12
|
|
13
|
-
auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
|
13
|
+
auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
|
14
14
|
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
15
15
|
|
16
16
|
auth_value_method :oauth_jwt_token_issuer, nil
|
17
17
|
|
18
|
-
|
18
|
+
configuration_module_eval do
|
19
|
+
define_method :oauth_applications_jws_jwk_column do
|
20
|
+
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
|
21
|
+
oauth_applications_jwks_column
|
22
|
+
end
|
23
|
+
define_method :oauth_applications_jws_jwk_label do
|
24
|
+
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
|
25
|
+
oauth_applications_jws_jwk_label
|
26
|
+
end
|
27
|
+
define_method :oauth_application_jws_jwk_param do
|
28
|
+
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
|
29
|
+
oauth_applications_jwks_param
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
auth_value_method :oauth_applications_subject_type_column, :subject_type
|
19
34
|
auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
|
35
|
+
auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
|
36
|
+
auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
|
37
|
+
auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
|
20
38
|
|
21
|
-
translatable_method :oauth_applications_jws_jwk_label, "JSON Web Keys"
|
22
39
|
translatable_method :oauth_applications_jwt_public_key_label, "Public key"
|
23
|
-
auth_value_method :oauth_application_jws_jwk_param, :jws_jwk
|
24
|
-
auth_value_method :oauth_application_jwt_public_key_param, :jwt_public_key
|
25
40
|
|
41
|
+
auth_value_method :oauth_jwt_keys, {}
|
26
42
|
auth_value_method :oauth_jwt_key, nil
|
27
43
|
auth_value_method :oauth_jwt_public_key, nil
|
28
|
-
auth_value_method :oauth_jwt_algorithm, "
|
44
|
+
auth_value_method :oauth_jwt_algorithm, "RS256"
|
29
45
|
|
30
46
|
auth_value_method :oauth_jwt_jwe_key, nil
|
31
47
|
auth_value_method :oauth_jwt_jwe_public_key, nil
|
@@ -119,13 +135,19 @@ module Rodauth
|
|
119
135
|
|
120
136
|
return super unless request_object && oauth_application
|
121
137
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
138
|
+
if (jwks = oauth_application_jwks)
|
139
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
140
|
+
else
|
141
|
+
redirect_response_error("invalid_request_object")
|
142
|
+
end
|
127
143
|
|
128
|
-
|
144
|
+
request_sig_enc_opts = {
|
145
|
+
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
146
|
+
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
147
|
+
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
148
|
+
}.compact
|
149
|
+
|
150
|
+
claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
|
129
151
|
|
130
152
|
redirect_response_error("invalid_request_object") unless claims
|
131
153
|
|
@@ -208,7 +230,12 @@ module Rodauth
|
|
208
230
|
end
|
209
231
|
|
210
232
|
def jwt_subject(oauth_token)
|
211
|
-
|
233
|
+
subject_type = if oauth_application
|
234
|
+
oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
|
235
|
+
else
|
236
|
+
oauth_jwt_subject_type
|
237
|
+
end
|
238
|
+
case subject_type
|
212
239
|
when "public"
|
213
240
|
oauth_token[oauth_tokens_account_id_column]
|
214
241
|
when "pairwise"
|
@@ -216,7 +243,7 @@ module Rodauth
|
|
216
243
|
application_id = oauth_token[oauth_tokens_oauth_application_id_column]
|
217
244
|
Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
|
218
245
|
else
|
219
|
-
raise StandardError, "unexpected subject (#{
|
246
|
+
raise StandardError, "unexpected subject (#{subject_type})"
|
220
247
|
end
|
221
248
|
end
|
222
249
|
|
@@ -245,11 +272,11 @@ module Rodauth
|
|
245
272
|
}
|
246
273
|
end
|
247
274
|
|
248
|
-
def oauth_server_metadata_body(path)
|
275
|
+
def oauth_server_metadata_body(path = nil)
|
249
276
|
metadata = super
|
250
277
|
metadata.merge! \
|
251
278
|
jwks_uri: jwks_url,
|
252
|
-
token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
|
279
|
+
token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
|
253
280
|
metadata
|
254
281
|
end
|
255
282
|
|
@@ -257,9 +284,9 @@ module Rodauth
|
|
257
284
|
@_jwt_key ||= oauth_jwt_key || begin
|
258
285
|
if oauth_application
|
259
286
|
|
260
|
-
if (
|
261
|
-
|
262
|
-
|
287
|
+
if (jwks = oauth_application_jwks)
|
288
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
|
289
|
+
jwks
|
263
290
|
else
|
264
291
|
oauth_application[oauth_applications_jwt_public_key_column]
|
265
292
|
end
|
@@ -267,6 +294,16 @@ module Rodauth
|
|
267
294
|
end
|
268
295
|
end
|
269
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
|
+
|
270
307
|
# Resource Server only!
|
271
308
|
#
|
272
309
|
# returns the jwks set from the authorization server.
|
@@ -319,44 +356,121 @@ module Rodauth
|
|
319
356
|
expected_aud == aud
|
320
357
|
end
|
321
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
|
+
|
322
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
|
+
]
|
323
413
|
|
324
414
|
def jwk_import(data)
|
325
415
|
JSON::JWK.new(data)
|
326
416
|
end
|
327
417
|
|
328
|
-
|
329
|
-
|
418
|
+
def jwt_encode(payload,
|
419
|
+
jwks: nil,
|
420
|
+
jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
421
|
+
signing_algorithm: oauth_jwt_algorithm,
|
422
|
+
encryption_algorithm: oauth_jwt_jwe_algorithm,
|
423
|
+
encryption_method: oauth_jwt_jwe_encryption_method)
|
330
424
|
payload[:jti] = generate_jti(payload)
|
331
425
|
jwt = JSON::JWT.new(payload)
|
332
|
-
jwk = JSON::JWK.new(_jwt_key)
|
333
426
|
|
334
|
-
|
427
|
+
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
428
|
+
key = key.first if key.is_a?(Array)
|
429
|
+
|
430
|
+
jwk = JSON::JWK.new(key || "")
|
431
|
+
|
432
|
+
jwt = jwt.sign(jwk, signing_algorithm)
|
335
433
|
jwt.kid = jwk.thumbprint
|
336
434
|
|
337
|
-
if
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
435
|
+
if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
|
436
|
+
jwk = JSON::JWK.new(jwk)
|
437
|
+
jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
|
438
|
+
jwe.to_s
|
439
|
+
elsif jwe_key
|
440
|
+
algorithm = encryption_algorithm.to_sym if encryption_algorithm
|
441
|
+
meth = encryption_method.to_sym if encryption_method
|
442
|
+
jwt.encrypt(jwe_key, algorithm, meth)
|
443
|
+
else
|
444
|
+
jwt.to_s
|
342
445
|
end
|
343
|
-
jwt.to_s
|
344
446
|
end
|
345
447
|
|
346
448
|
def jwt_decode(
|
347
449
|
token,
|
450
|
+
jwks: nil,
|
348
451
|
jws_key: oauth_jwt_public_key || _jwt_key,
|
452
|
+
jws_algorithm: oauth_jwt_algorithm,
|
453
|
+
jwe_key: oauth_jwt_jwe_key,
|
454
|
+
jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
|
455
|
+
jws_encryption_method: oauth_jwt_jwe_encryption_method,
|
349
456
|
verify_claims: true,
|
350
457
|
verify_jti: true,
|
351
458
|
verify_iss: true,
|
352
459
|
verify_aud: false,
|
353
460
|
**
|
354
461
|
)
|
355
|
-
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if
|
462
|
+
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key
|
356
463
|
|
357
464
|
claims = if is_authorization_server?
|
358
465
|
if oauth_jwt_legacy_public_key
|
359
466
|
JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
|
467
|
+
elsif jwks
|
468
|
+
enc_algs = [jws_encryption_algorithm].compact
|
469
|
+
enc_meths = [jws_encryption_method].compact
|
470
|
+
sig_algs = [jws_algorithm].compact.map(&:to_sym)
|
471
|
+
jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
|
472
|
+
jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
|
473
|
+
jws
|
360
474
|
elsif jws_key
|
361
475
|
JSON::JWT.decode(token, jws_key)
|
362
476
|
end
|
@@ -390,20 +504,43 @@ module Rodauth
|
|
390
504
|
end
|
391
505
|
|
392
506
|
elsif defined?(JWT)
|
393
|
-
|
394
507
|
# ruby-jwt
|
508
|
+
require "rodauth/oauth/jwe_extensions" if defined?(JWE)
|
509
|
+
|
510
|
+
auth_value_method :oauth_jwt_algorithms_supported, %w[
|
511
|
+
HS256 HS384 HS512 HS512256
|
512
|
+
RS256 RS384 RS512
|
513
|
+
ED25519
|
514
|
+
ES256 ES384 ES512
|
515
|
+
PS256 PS384 PS512
|
516
|
+
]
|
517
|
+
|
518
|
+
auth_value_methods(
|
519
|
+
:oauth_jwt_jwe_algorithms_supported,
|
520
|
+
:oauth_jwt_jwe_encryption_methods_supported
|
521
|
+
)
|
522
|
+
|
523
|
+
def oauth_jwt_jwe_algorithms_supported
|
524
|
+
JWE::VALID_ALG
|
525
|
+
end
|
526
|
+
|
527
|
+
def oauth_jwt_jwe_encryption_methods_supported
|
528
|
+
JWE::VALID_ENC
|
529
|
+
end
|
395
530
|
|
396
531
|
def jwk_import(data)
|
397
532
|
JWT::JWK.import(data).keypair
|
398
533
|
end
|
399
534
|
|
400
|
-
def jwt_encode(payload)
|
535
|
+
def jwt_encode(payload, signing_algorithm: oauth_jwt_algorithm)
|
401
536
|
headers = {}
|
402
537
|
|
403
|
-
key = _jwt_key
|
538
|
+
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
539
|
+
key = key.first if key.is_a?(Array)
|
404
540
|
|
405
|
-
|
406
|
-
|
541
|
+
case key
|
542
|
+
when OpenSSL::PKey::PKey
|
543
|
+
jwk = JWT::JWK.new(key)
|
407
544
|
headers[:kid] = jwk.kid
|
408
545
|
|
409
546
|
key = jwk.keypair
|
@@ -411,23 +548,44 @@ module Rodauth
|
|
411
548
|
|
412
549
|
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
413
550
|
payload[:jti] = generate_jti(payload)
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
551
|
+
JWT.encode(payload, key, signing_algorithm, headers)
|
552
|
+
end
|
553
|
+
|
554
|
+
if defined?(JWE)
|
555
|
+
def jwt_encode_with_jwe(
|
556
|
+
payload,
|
557
|
+
jwks: nil,
|
558
|
+
jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
559
|
+
encryption_algorithm: oauth_jwt_jwe_algorithm,
|
560
|
+
encryption_method: oauth_jwt_jwe_encryption_method, **args
|
561
|
+
)
|
562
|
+
|
563
|
+
token = jwt_encode_without_jwe(payload, **args)
|
564
|
+
|
565
|
+
return token unless encryption_algorithm && encryption_method
|
566
|
+
|
567
|
+
if jwks && jwks.any? { |k| k[:use] == "enc" }
|
568
|
+
JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
|
569
|
+
elsif jwe_key
|
570
|
+
params = {
|
571
|
+
zip: "DEF",
|
572
|
+
copyright: oauth_jwt_jwe_copyright
|
573
|
+
}
|
574
|
+
params[:enc] = encryption_method if encryption_method
|
575
|
+
params[:alg] = encryption_algorithm if encryption_algorithm
|
576
|
+
JWE.encrypt(token, jwe_key, **params)
|
577
|
+
else
|
578
|
+
token
|
579
|
+
end
|
424
580
|
end
|
425
581
|
|
426
|
-
|
582
|
+
alias_method :jwt_encode_without_jwe, :jwt_encode
|
583
|
+
alias_method :jwt_encode, :jwt_encode_with_jwe
|
427
584
|
end
|
428
585
|
|
429
586
|
def jwt_decode(
|
430
587
|
token,
|
588
|
+
jwks: nil,
|
431
589
|
jws_key: oauth_jwt_public_key || _jwt_key,
|
432
590
|
jws_algorithm: oauth_jwt_algorithm,
|
433
591
|
verify_claims: true,
|
@@ -435,9 +593,6 @@ module Rodauth
|
|
435
593
|
verify_iss: true,
|
436
594
|
verify_aud: false
|
437
595
|
)
|
438
|
-
# decrypt jwe
|
439
|
-
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
440
|
-
|
441
596
|
# verifying the JWT implies verifying:
|
442
597
|
#
|
443
598
|
# issuer: check that server generated the token
|
@@ -465,6 +620,8 @@ module Rodauth
|
|
465
620
|
if oauth_jwt_legacy_public_key
|
466
621
|
algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
467
622
|
JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
|
623
|
+
elsif jwks
|
624
|
+
JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
|
468
625
|
elsif jws_key
|
469
626
|
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
|
470
627
|
end
|
@@ -480,6 +637,33 @@ module Rodauth
|
|
480
637
|
nil
|
481
638
|
end
|
482
639
|
|
640
|
+
if defined?(JWE)
|
641
|
+
def jwt_decode_with_jwe(
|
642
|
+
token,
|
643
|
+
jwks: nil,
|
644
|
+
jwe_key: oauth_jwt_jwe_key,
|
645
|
+
jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
|
646
|
+
jws_encryption_method: oauth_jwt_jwe_encryption_method,
|
647
|
+
**args
|
648
|
+
)
|
649
|
+
|
650
|
+
token = if jwks && jwks.any? { |k| k[:use] == "enc" }
|
651
|
+
JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
|
652
|
+
elsif jwe_key
|
653
|
+
JWE.decrypt(token, jwe_key)
|
654
|
+
else
|
655
|
+
token
|
656
|
+
end
|
657
|
+
|
658
|
+
jwt_decode_without_jwe(token, jwks: jwks, **args)
|
659
|
+
rescue JWE::DecodeError => e
|
660
|
+
jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
|
661
|
+
end
|
662
|
+
|
663
|
+
alias_method :jwt_decode_without_jwe, :jwt_decode
|
664
|
+
alias_method :jwt_decode, :jwt_decode_with_jwe
|
665
|
+
end
|
666
|
+
|
483
667
|
def jwks_set
|
484
668
|
@jwks_set ||= [
|
485
669
|
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
@@ -518,5 +702,20 @@ module Rodauth
|
|
518
702
|
|
519
703
|
super
|
520
704
|
end
|
705
|
+
|
706
|
+
def jwt_response_success(jwt, cache = false)
|
707
|
+
response.status = 200
|
708
|
+
response["Content-Type"] ||= "application/jwt"
|
709
|
+
if cache
|
710
|
+
# defaulting to 1-day for everyone, for now at least
|
711
|
+
max_age = 60 * 60 * 24
|
712
|
+
response["Cache-Control"] = "private, max-age=#{max_age}"
|
713
|
+
else
|
714
|
+
response["Cache-Control"] = "no-store"
|
715
|
+
response["Pragma"] = "no-cache"
|
716
|
+
end
|
717
|
+
response.write(jwt)
|
718
|
+
request.halt
|
719
|
+
end
|
521
720
|
end
|
522
721
|
end
|