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
@@ -0,0 +1,435 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
require "rodauth/oauth/http_extensions"
|
5
|
+
|
6
|
+
module Rodauth
|
7
|
+
Feature.define(:oauth_jwt_base, :OauthJwtBase) do
|
8
|
+
depends :oauth_base
|
9
|
+
|
10
|
+
auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
|
11
|
+
auth_value_method :oauth_application_jwks_param, "jwks"
|
12
|
+
|
13
|
+
auth_value_method :oauth_jwt_keys, {}
|
14
|
+
auth_value_method :oauth_jwt_public_keys, {}
|
15
|
+
|
16
|
+
auth_value_method :oauth_jwt_jwe_keys, {}
|
17
|
+
auth_value_method :oauth_jwt_jwe_public_keys, {}
|
18
|
+
|
19
|
+
auth_value_method :oauth_jwt_jwe_copyright, nil
|
20
|
+
|
21
|
+
auth_value_methods(
|
22
|
+
:jwt_encode,
|
23
|
+
:jwt_decode,
|
24
|
+
:jwt_decode_no_key,
|
25
|
+
:generate_jti,
|
26
|
+
:oauth_jwt_issuer,
|
27
|
+
:oauth_jwt_audience
|
28
|
+
)
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def oauth_jwt_issuer
|
33
|
+
# The JWT MUST contain an "iss" (issuer) claim that contains a
|
34
|
+
# unique identifier for the entity that issued the JWT.
|
35
|
+
@oauth_jwt_issuer ||= authorization_server_url
|
36
|
+
end
|
37
|
+
|
38
|
+
def oauth_jwt_audience
|
39
|
+
# The JWT MUST contain an "aud" (audience) claim containing a
|
40
|
+
# value that identifies the authorization server as an intended
|
41
|
+
# audience. The token endpoint URL of the authorization server
|
42
|
+
# MAY be used as a value for an "aud" element to identify the
|
43
|
+
# authorization server as an intended audience of the JWT.
|
44
|
+
@oauth_jwt_audience ||= if is_authorization_server?
|
45
|
+
oauth_application[oauth_applications_client_id_column]
|
46
|
+
else
|
47
|
+
metadata = authorization_server_metadata
|
48
|
+
|
49
|
+
return unless metadata
|
50
|
+
|
51
|
+
metadata[:token_endpoint]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def grant_from_application?(grant_or_claims, oauth_application)
|
56
|
+
return super if grant_or_claims[oauth_grants_id_column]
|
57
|
+
|
58
|
+
if grant_or_claims["client_id"]
|
59
|
+
grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
|
60
|
+
else
|
61
|
+
Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def jwt_subject(oauth_grant, client_application = oauth_application)
|
66
|
+
oauth_grant[oauth_grants_account_id_column] || client_application[oauth_applications_client_id_column]
|
67
|
+
end
|
68
|
+
|
69
|
+
def oauth_server_metadata_body(path = nil)
|
70
|
+
metadata = super
|
71
|
+
metadata.merge! \
|
72
|
+
token_endpoint_auth_signing_alg_values_supported: oauth_jwt_keys.keys.uniq
|
73
|
+
metadata
|
74
|
+
end
|
75
|
+
|
76
|
+
def _jwt_key
|
77
|
+
@_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
|
78
|
+
end
|
79
|
+
|
80
|
+
def _jwt_public_key
|
81
|
+
@_jwt_public_key ||= if oauth_application
|
82
|
+
oauth_application_jwks(oauth_application)
|
83
|
+
else
|
84
|
+
_jwt_key
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Resource Server only!
|
89
|
+
#
|
90
|
+
# returns the jwks set from the authorization server.
|
91
|
+
def auth_server_jwks_set
|
92
|
+
metadata = authorization_server_metadata
|
93
|
+
|
94
|
+
return unless metadata && (jwks_uri = metadata[:jwks_uri])
|
95
|
+
|
96
|
+
jwks_uri = URI(jwks_uri)
|
97
|
+
|
98
|
+
http_request_with_cache(jwks_uri)
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_jti(payload)
|
102
|
+
# Use the key and iat to create a unique key per request to prevent replay attacks
|
103
|
+
jti_raw = [
|
104
|
+
payload[:aud] || payload["aud"],
|
105
|
+
payload[:iat] || payload["iat"]
|
106
|
+
].join(":").to_s
|
107
|
+
Digest::SHA256.hexdigest(jti_raw)
|
108
|
+
end
|
109
|
+
|
110
|
+
def verify_jti(jti, claims)
|
111
|
+
generate_jti(claims) == jti
|
112
|
+
end
|
113
|
+
|
114
|
+
def verify_aud(expected_aud, aud)
|
115
|
+
expected_aud == aud
|
116
|
+
end
|
117
|
+
|
118
|
+
def oauth_application_jwks(oauth_application)
|
119
|
+
jwks = oauth_application[oauth_applications_jwks_column]
|
120
|
+
|
121
|
+
if jwks
|
122
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
123
|
+
return jwks
|
124
|
+
end
|
125
|
+
|
126
|
+
jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
|
127
|
+
|
128
|
+
return unless jwks_uri
|
129
|
+
|
130
|
+
jwks_uri = URI(jwks_uri)
|
131
|
+
|
132
|
+
http_request_with_cache(jwks_uri)
|
133
|
+
end
|
134
|
+
|
135
|
+
if defined?(JSON::JWT)
|
136
|
+
# json-jwt
|
137
|
+
|
138
|
+
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
|
139
|
+
HS256 HS384 HS512
|
140
|
+
RS256 RS384 RS512
|
141
|
+
PS256 PS384 PS512
|
142
|
+
ES256 ES384 ES512 ES256K
|
143
|
+
]
|
144
|
+
auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
|
145
|
+
RSA1_5 RSA-OAEP dir A128KW A256KW
|
146
|
+
]
|
147
|
+
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
|
148
|
+
A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
|
149
|
+
]
|
150
|
+
|
151
|
+
def jwk_export(key)
|
152
|
+
JSON::JWK.new(key)
|
153
|
+
end
|
154
|
+
|
155
|
+
def jwt_encode(payload,
|
156
|
+
jwks: nil,
|
157
|
+
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
|
158
|
+
encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
|
159
|
+
jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
|
160
|
+
encryption_method]],
|
161
|
+
signing_algorithm: oauth_jwt_keys.keys.first)
|
162
|
+
payload[:jti] = generate_jti(payload)
|
163
|
+
jwt = JSON::JWT.new(payload)
|
164
|
+
|
165
|
+
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
166
|
+
key = key.first if key.is_a?(Array)
|
167
|
+
|
168
|
+
jwk = JSON::JWK.new(key || "")
|
169
|
+
|
170
|
+
jwt = jwt.sign(jwk, signing_algorithm)
|
171
|
+
jwt.kid = jwk.thumbprint
|
172
|
+
|
173
|
+
if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
|
174
|
+
jwk = JSON::JWK.new(jwk)
|
175
|
+
jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
|
176
|
+
jwe.to_s
|
177
|
+
elsif jwe_key
|
178
|
+
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
179
|
+
algorithm = encryption_algorithm.to_sym if encryption_algorithm
|
180
|
+
meth = encryption_method.to_sym if encryption_method
|
181
|
+
jwt.encrypt(jwe_key, algorithm, meth)
|
182
|
+
else
|
183
|
+
jwt.to_s
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def jwt_decode(
|
188
|
+
token,
|
189
|
+
jwks: nil,
|
190
|
+
jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
|
191
|
+
jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
|
192
|
+
jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
|
193
|
+
jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
|
194
|
+
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
|
195
|
+
verify_claims: true,
|
196
|
+
verify_jti: true,
|
197
|
+
verify_iss: true,
|
198
|
+
verify_aud: true,
|
199
|
+
**
|
200
|
+
)
|
201
|
+
jws_key = jws_key.first if jws_key.is_a?(Array)
|
202
|
+
|
203
|
+
if jwe_key
|
204
|
+
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
205
|
+
token = JSON::JWT.decode(token, jwe_key).plain_text
|
206
|
+
end
|
207
|
+
|
208
|
+
claims = if is_authorization_server?
|
209
|
+
if jwks
|
210
|
+
enc_algs = [jws_encryption_algorithm].compact
|
211
|
+
enc_meths = [jws_encryption_method].compact
|
212
|
+
|
213
|
+
sig_algs = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
214
|
+
sig_algs = sig_algs.compact.map(&:to_sym)
|
215
|
+
|
216
|
+
jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
|
217
|
+
jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
|
218
|
+
jws
|
219
|
+
elsif jws_key
|
220
|
+
JSON::JWT.decode(token, jws_key)
|
221
|
+
end
|
222
|
+
elsif (jwks = auth_server_jwks_set)
|
223
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
224
|
+
end
|
225
|
+
|
226
|
+
now = Time.now
|
227
|
+
if verify_claims && (
|
228
|
+
(!claims[:exp] || Time.at(claims[:exp]) < now) &&
|
229
|
+
(claims[:nbf] && Time.at(claims[:nbf]) < now) &&
|
230
|
+
(claims[:iat] && Time.at(claims[:iat]) < now) &&
|
231
|
+
(verify_iss && claims[:iss] != oauth_jwt_issuer) &&
|
232
|
+
(verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
|
233
|
+
(verify_jti && !verify_jti(claims[:jti], claims))
|
234
|
+
)
|
235
|
+
return
|
236
|
+
end
|
237
|
+
|
238
|
+
claims
|
239
|
+
rescue JSON::JWT::Exception
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
|
243
|
+
def jwt_decode_no_key(token)
|
244
|
+
jws = JSON::JWT.decode(token, :skip_verification)
|
245
|
+
[jws.to_h, jws.header]
|
246
|
+
end
|
247
|
+
elsif defined?(JWT)
|
248
|
+
# ruby-jwt
|
249
|
+
require "rodauth/oauth/jwe_extensions" if defined?(JWE)
|
250
|
+
|
251
|
+
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
|
252
|
+
HS256 HS384 HS512 HS512256
|
253
|
+
RS256 RS384 RS512
|
254
|
+
ED25519
|
255
|
+
ES256 ES384 ES512
|
256
|
+
PS256 PS384 PS512
|
257
|
+
]
|
258
|
+
|
259
|
+
if defined?(JWE)
|
260
|
+
auth_value_methods(
|
261
|
+
:oauth_jwt_jwe_algorithms_supported,
|
262
|
+
:oauth_jwt_jwe_encryption_methods_supported
|
263
|
+
)
|
264
|
+
|
265
|
+
def oauth_jwt_jwe_algorithms_supported
|
266
|
+
JWE::VALID_ALG
|
267
|
+
end
|
268
|
+
|
269
|
+
def oauth_jwt_jwe_encryption_methods_supported
|
270
|
+
JWE::VALID_ENC
|
271
|
+
end
|
272
|
+
else
|
273
|
+
auth_value_method :oauth_jwt_jwe_algorithms_supported, []
|
274
|
+
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
|
275
|
+
end
|
276
|
+
|
277
|
+
def jwk_export(key)
|
278
|
+
JWT::JWK.new(key).export
|
279
|
+
end
|
280
|
+
|
281
|
+
def jwt_encode(payload,
|
282
|
+
signing_algorithm: oauth_jwt_keys.keys.first)
|
283
|
+
headers = {}
|
284
|
+
|
285
|
+
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
286
|
+
key = key.first if key.is_a?(Array)
|
287
|
+
|
288
|
+
case key
|
289
|
+
when OpenSSL::PKey::PKey
|
290
|
+
jwk = JWT::JWK.new(key)
|
291
|
+
headers[:kid] = jwk.kid
|
292
|
+
|
293
|
+
key = jwk.keypair
|
294
|
+
end
|
295
|
+
|
296
|
+
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
297
|
+
payload[:jti] = generate_jti(payload)
|
298
|
+
JWT.encode(payload, key, signing_algorithm, headers)
|
299
|
+
end
|
300
|
+
|
301
|
+
if defined?(JWE)
|
302
|
+
def jwt_encode_with_jwe(
|
303
|
+
payload,
|
304
|
+
jwks: nil,
|
305
|
+
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
|
306
|
+
encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
|
307
|
+
jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]],
|
308
|
+
**args
|
309
|
+
)
|
310
|
+
token = jwt_encode_without_jwe(payload, **args)
|
311
|
+
|
312
|
+
return token unless encryption_algorithm && encryption_method
|
313
|
+
|
314
|
+
if jwks && jwks.any? { |k| k[:use] == "enc" }
|
315
|
+
JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
|
316
|
+
elsif jwe_key
|
317
|
+
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
318
|
+
params = {
|
319
|
+
zip: "DEF",
|
320
|
+
copyright: oauth_jwt_jwe_copyright
|
321
|
+
}
|
322
|
+
params[:enc] = encryption_method if encryption_method
|
323
|
+
params[:alg] = encryption_algorithm if encryption_algorithm
|
324
|
+
JWE.encrypt(token, jwe_key, **params)
|
325
|
+
else
|
326
|
+
token
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
alias_method :jwt_encode_without_jwe, :jwt_encode
|
331
|
+
alias_method :jwt_encode, :jwt_encode_with_jwe
|
332
|
+
end
|
333
|
+
|
334
|
+
def jwt_decode(
|
335
|
+
token,
|
336
|
+
jwks: nil,
|
337
|
+
jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
|
338
|
+
jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
|
339
|
+
verify_claims: true,
|
340
|
+
verify_jti: true,
|
341
|
+
verify_iss: true,
|
342
|
+
verify_aud: true
|
343
|
+
)
|
344
|
+
jws_key = jws_key.first if jws_key.is_a?(Array)
|
345
|
+
|
346
|
+
# verifying the JWT implies verifying:
|
347
|
+
#
|
348
|
+
# issuer: check that server generated the token
|
349
|
+
# aud: check the audience field (client is who he says he is)
|
350
|
+
# iat: check that the token didn't expire
|
351
|
+
#
|
352
|
+
# subject can't be verified automatically without having access to the account id,
|
353
|
+
# which we don't because that's the whole point.
|
354
|
+
#
|
355
|
+
verify_claims_params = if verify_claims
|
356
|
+
{
|
357
|
+
verify_iss: verify_iss,
|
358
|
+
iss: oauth_jwt_issuer,
|
359
|
+
# can't use stock aud verification, as it's dependent on the client application id
|
360
|
+
verify_aud: false,
|
361
|
+
verify_jti: (verify_jti ? method(:verify_jti) : false),
|
362
|
+
verify_iat: true
|
363
|
+
}
|
364
|
+
else
|
365
|
+
{}
|
366
|
+
end
|
367
|
+
|
368
|
+
# decode jwt
|
369
|
+
claims = if is_authorization_server?
|
370
|
+
if jwks
|
371
|
+
algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
372
|
+
JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params).first
|
373
|
+
elsif jws_key
|
374
|
+
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
|
375
|
+
end
|
376
|
+
elsif (jwks = auth_server_jwks_set)
|
377
|
+
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
378
|
+
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
|
379
|
+
end
|
380
|
+
|
381
|
+
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
|
382
|
+
|
383
|
+
claims
|
384
|
+
rescue JWT::DecodeError, JWT::JWKError
|
385
|
+
nil
|
386
|
+
end
|
387
|
+
|
388
|
+
if defined?(JWE)
|
389
|
+
def jwt_decode_with_jwe(
|
390
|
+
token,
|
391
|
+
jwks: nil,
|
392
|
+
jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
|
393
|
+
jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
|
394
|
+
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
|
395
|
+
**args
|
396
|
+
)
|
397
|
+
|
398
|
+
token = if jwks && jwks.any? { |k| k[:use] == "enc" }
|
399
|
+
JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
|
400
|
+
elsif jwe_key
|
401
|
+
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
|
402
|
+
JWE.decrypt(token, jwe_key)
|
403
|
+
else
|
404
|
+
token
|
405
|
+
end
|
406
|
+
|
407
|
+
jwt_decode_without_jwe(token, jwks: jwks, **args)
|
408
|
+
rescue JWE::DecodeError => e
|
409
|
+
jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
|
410
|
+
end
|
411
|
+
|
412
|
+
alias_method :jwt_decode_without_jwe, :jwt_decode
|
413
|
+
alias_method :jwt_decode, :jwt_decode_with_jwe
|
414
|
+
end
|
415
|
+
|
416
|
+
def jwt_decode_no_key(token)
|
417
|
+
JWT.decode(token, nil, false)
|
418
|
+
end
|
419
|
+
else
|
420
|
+
# :nocov:
|
421
|
+
def jwk_export(_key)
|
422
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
423
|
+
end
|
424
|
+
|
425
|
+
def jwt_encode(_token)
|
426
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
427
|
+
end
|
428
|
+
|
429
|
+
def jwt_decode(_token, **)
|
430
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
431
|
+
end
|
432
|
+
# :nocov:
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "rodauth/oauth
|
4
|
-
require "rodauth/oauth/ttl_store"
|
3
|
+
require "rodauth/oauth"
|
5
4
|
|
6
5
|
module Rodauth
|
7
6
|
Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
|
@@ -13,6 +12,18 @@ module Rodauth
|
|
13
12
|
:account_from_jwt_bearer_assertion
|
14
13
|
)
|
15
14
|
|
15
|
+
def oauth_token_endpoint_auth_methods_supported
|
16
|
+
if oauth_applications_client_secret_hash_column.nil?
|
17
|
+
super | %w[client_secret_jwt private_key_jwt urn:ietf:params:oauth:client-assertion-type:jwt-bearer]
|
18
|
+
else
|
19
|
+
super | %w[private_key_jwt]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def oauth_grant_types_supported
|
24
|
+
super | %w[urn:ietf:params:oauth:grant-type:jwt-bearer]
|
25
|
+
end
|
26
|
+
|
16
27
|
private
|
17
28
|
|
18
29
|
def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
|
@@ -26,13 +37,37 @@ module Rodauth
|
|
26
37
|
end
|
27
38
|
|
28
39
|
def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
|
29
|
-
claims =
|
40
|
+
claims, header = jwt_decode_no_key(assertion)
|
41
|
+
|
42
|
+
client_id = claims["sub"]
|
43
|
+
|
44
|
+
case header["alg"]
|
45
|
+
when "none"
|
46
|
+
# do not accept jwts with no alg set
|
47
|
+
authorization_required
|
48
|
+
when /\AHS/
|
49
|
+
require_oauth_application_from_client_secret_jwt(client_id, assertion, header["alg"])
|
50
|
+
else
|
51
|
+
require_oauth_application_from_private_key_jwt(client_id, assertion)
|
52
|
+
end
|
53
|
+
end
|
30
54
|
|
31
|
-
|
55
|
+
def require_oauth_application_from_client_secret_jwt(client_id, assertion, alg)
|
56
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
57
|
+
authorization_required unless supports_auth_method?(oauth_application, "client_secret_jwt")
|
58
|
+
client_secret = oauth_application[oauth_applications_client_secret_column]
|
59
|
+
claims = jwt_assertion(assertion, jws_key: client_secret, jws_algorithm: alg)
|
60
|
+
authorization_required unless claims && claims["iss"] == client_id
|
61
|
+
oauth_application
|
62
|
+
end
|
32
63
|
|
33
|
-
|
34
|
-
|
35
|
-
)
|
64
|
+
def require_oauth_application_from_private_key_jwt(client_id, assertion)
|
65
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
66
|
+
authorization_required unless supports_auth_method?(oauth_application, "private_key_jwt")
|
67
|
+
jwks = oauth_application_jwks(oauth_application)
|
68
|
+
claims = jwt_assertion(assertion, jwks: jwks)
|
69
|
+
authorization_required unless claims
|
70
|
+
oauth_application
|
36
71
|
end
|
37
72
|
|
38
73
|
def account_from_jwt_bearer_assertion(assertion)
|
@@ -43,18 +78,11 @@ module Rodauth
|
|
43
78
|
account_from_bearer_assertion_subject(claims["sub"])
|
44
79
|
end
|
45
80
|
|
46
|
-
def jwt_assertion(assertion)
|
47
|
-
claims = jwt_decode(assertion, verify_iss: false, verify_aud: false)
|
48
|
-
return unless verify_aud(
|
81
|
+
def jwt_assertion(assertion, **kwargs)
|
82
|
+
claims = jwt_decode(assertion, verify_iss: false, verify_aud: false, **kwargs)
|
83
|
+
return unless verify_aud(request.url, claims["aud"])
|
49
84
|
|
50
85
|
claims
|
51
86
|
end
|
52
|
-
|
53
|
-
def oauth_server_metadata_body(*)
|
54
|
-
super.tap do |data|
|
55
|
-
data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
56
|
-
data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
57
|
-
end
|
58
|
-
end
|
59
87
|
end
|
60
88
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
require "rodauth/oauth/http_extensions"
|
5
|
+
|
6
|
+
module Rodauth
|
7
|
+
Feature.define(:oauth_jwt_jwks, :OauthJwtJwks) do
|
8
|
+
depends :oauth_jwt_base
|
9
|
+
|
10
|
+
auth_value_methods(:jwks_set)
|
11
|
+
|
12
|
+
auth_server_route(:jwks) do |r|
|
13
|
+
before_jwks_route
|
14
|
+
|
15
|
+
r.get do
|
16
|
+
json_response_success({ keys: jwks_set }, true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def oauth_server_metadata_body(path = nil)
|
23
|
+
metadata = super
|
24
|
+
metadata.merge!(jwks_uri: jwks_url)
|
25
|
+
metadata
|
26
|
+
end
|
27
|
+
|
28
|
+
def jwks_set
|
29
|
+
@jwks_set ||= [
|
30
|
+
*(
|
31
|
+
unless oauth_jwt_public_keys.empty?
|
32
|
+
oauth_jwt_public_keys.flat_map { |algo, pkeys| Array(pkeys).map { |pkey| jwk_export(pkey).merge(use: "sig", alg: algo) } }
|
33
|
+
end
|
34
|
+
),
|
35
|
+
*(
|
36
|
+
unless oauth_jwt_jwe_public_keys.empty?
|
37
|
+
oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
|
38
|
+
Array(pkeys).map do |pkey|
|
39
|
+
jwk_export(pkey).merge(use: "enc", alg: algo)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
)
|
44
|
+
].compact
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt_secured_authorization_request, :OauthJwtSecuredAuthorizationRequest) do
|
7
|
+
depends :oauth_authorize_base, :oauth_jwt_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
|
10
|
+
auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
|
11
|
+
auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
|
12
|
+
|
13
|
+
translatable_method :oauth_request_uri_not_supported_message, "request uri is unsupported"
|
14
|
+
translatable_method :oauth_invalid_request_object_message, "request object is invalid"
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# /authorize
|
19
|
+
|
20
|
+
def validate_authorize_params
|
21
|
+
# TODO: add support for requst_uri
|
22
|
+
redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
|
23
|
+
|
24
|
+
request_object = param_or_nil("request")
|
25
|
+
|
26
|
+
return super unless request_object && oauth_application
|
27
|
+
|
28
|
+
if (jwks = oauth_application_jwks(oauth_application))
|
29
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
30
|
+
else
|
31
|
+
redirect_response_error("invalid_request_object")
|
32
|
+
end
|
33
|
+
|
34
|
+
request_sig_enc_opts = {
|
35
|
+
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
36
|
+
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
37
|
+
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
38
|
+
}.compact
|
39
|
+
|
40
|
+
claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, verify_aud: false, **request_sig_enc_opts)
|
41
|
+
|
42
|
+
redirect_response_error("invalid_request_object") unless claims
|
43
|
+
|
44
|
+
# If signed, the Authorization Request
|
45
|
+
# Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
|
46
|
+
# as members, with their semantics being the same as defined in the JWT
|
47
|
+
# [RFC7519] specification. The value of "aud" should be the value of
|
48
|
+
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
49
|
+
# [RFC8414].
|
50
|
+
claims.delete("iss")
|
51
|
+
audience = claims.delete("aud")
|
52
|
+
|
53
|
+
redirect_response_error("invalid_request_object") if audience && audience != oauth_jwt_issuer
|
54
|
+
|
55
|
+
claims.each do |k, v|
|
56
|
+
request.params[k.to_s] = v
|
57
|
+
end
|
58
|
+
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|