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
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_pushed_authorization_request, :OauthJwtPushedAuthorizationRequest) do
|
7
|
+
depends :oauth_authorize_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_require_pushed_authorization_requests, false
|
10
|
+
auth_value_method :oauth_applications_require_pushed_authorization_requests_column, :require_pushed_authorization_requests
|
11
|
+
auth_value_method :oauth_pushed_authorization_request_expires_in, 90 # 90 seconds
|
12
|
+
auth_value_method :oauth_require_pushed_authorization_request_iss_request_object, true
|
13
|
+
|
14
|
+
auth_value_method :oauth_pushed_authorization_requests_table, :oauth_pushed_requests
|
15
|
+
|
16
|
+
%i[
|
17
|
+
oauth_application_id params code expires_in
|
18
|
+
].each do |column|
|
19
|
+
auth_value_method :"oauth_pushed_authorization_requests_#{column}_column", column
|
20
|
+
end
|
21
|
+
|
22
|
+
# /par
|
23
|
+
auth_server_route(:par) do |r|
|
24
|
+
require_oauth_application
|
25
|
+
before_par_route
|
26
|
+
|
27
|
+
r.post do
|
28
|
+
validate_par_params
|
29
|
+
|
30
|
+
ds = db[oauth_pushed_authorization_requests_table]
|
31
|
+
|
32
|
+
code = oauth_unique_id_generator
|
33
|
+
push_request_params = {
|
34
|
+
oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
35
|
+
oauth_pushed_authorization_requests_code_column => code,
|
36
|
+
oauth_pushed_authorization_requests_params_column => URI.encode_www_form(request.params),
|
37
|
+
oauth_pushed_authorization_requests_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
|
38
|
+
seconds: oauth_pushed_authorization_request_expires_in)
|
39
|
+
}
|
40
|
+
|
41
|
+
rescue_from_uniqueness_error do
|
42
|
+
ds.insert(push_request_params)
|
43
|
+
end
|
44
|
+
|
45
|
+
json_response_success(
|
46
|
+
"request_uri" => "urn:ietf:params:oauth:request_uri:#{code}",
|
47
|
+
"expires_in" => oauth_pushed_authorization_request_expires_in
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_csrf?
|
53
|
+
case request.path
|
54
|
+
when par_path
|
55
|
+
false
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def validate_par_params
|
64
|
+
# https://datatracker.ietf.org/doc/html/rfc9126#section-2.1
|
65
|
+
# The request_uri authorization request parameter is one exception, and it MUST NOT be provided.
|
66
|
+
redirect_response_error("invalid_request") if param_or_nil("request_uri")
|
67
|
+
|
68
|
+
if (request_object = param_or_nil("request")) && features.include?(:oauth_jwt_secured_authorization_request)
|
69
|
+
claims = decode_request_object(request_object)
|
70
|
+
|
71
|
+
# https://datatracker.ietf.org/doc/html/rfc9126#section-3-5.3
|
72
|
+
# reject the request if the authenticated client_id does not match the client_id claim in the Request Object
|
73
|
+
if (client_id = claims["client_id"]) && (client_id != oauth_application[oauth_applications_client_id_column])
|
74
|
+
redirect_response_error("invalid_request_object")
|
75
|
+
end
|
76
|
+
|
77
|
+
# requiring the iss claim to match the client_id is at the discretion of the authorization server
|
78
|
+
if oauth_require_pushed_authorization_request_iss_request_object &&
|
79
|
+
(iss = claims.delete("iss")) &&
|
80
|
+
iss != oauth_application[oauth_applications_client_id_column]
|
81
|
+
redirect_response_error("invalid_request_object")
|
82
|
+
end
|
83
|
+
|
84
|
+
if (aud = claims.delete("aud")) && !verify_aud(aud, oauth_jwt_issuer)
|
85
|
+
redirect_response_error("invalid_request_object")
|
86
|
+
end
|
87
|
+
|
88
|
+
claims.delete("exp")
|
89
|
+
request.params.delete("request")
|
90
|
+
|
91
|
+
claims.each do |k, v|
|
92
|
+
request.params[k.to_s] = v
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
validate_authorize_params
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate_authorize_params
|
100
|
+
return super unless request.get? && request.path == authorize_path
|
101
|
+
|
102
|
+
if (request_uri = param_or_nil("request_uri"))
|
103
|
+
code = request_uri.delete_prefix("urn:ietf:params:oauth:request_uri:")
|
104
|
+
|
105
|
+
table = oauth_pushed_authorization_requests_table
|
106
|
+
ds = db[table]
|
107
|
+
|
108
|
+
pushed_request = ds.where(
|
109
|
+
oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
110
|
+
oauth_pushed_authorization_requests_code_column => code
|
111
|
+
).where(
|
112
|
+
Sequel.expr(Sequel[table][oauth_pushed_authorization_requests_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP
|
113
|
+
).first
|
114
|
+
|
115
|
+
redirect_response_error("invalid_request") unless pushed_request
|
116
|
+
|
117
|
+
URI.decode_www_form(pushed_request[oauth_pushed_authorization_requests_params_column]).each do |k, v|
|
118
|
+
request.params[k.to_s] = v
|
119
|
+
end
|
120
|
+
|
121
|
+
elsif oauth_require_pushed_authorization_requests ||
|
122
|
+
(oauth_application && oauth_application[oauth_applications_require_pushed_authorization_requests_column])
|
123
|
+
redirect_authorize_error("request_uri")
|
124
|
+
end
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def oauth_server_metadata_body(*)
|
129
|
+
super.tap do |data|
|
130
|
+
data[:require_pushed_authorization_requests] = oauth_require_pushed_authorization_requests
|
131
|
+
data[:pushed_authorization_request_endpoint] = par_url
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "ipaddr"
|
5
|
+
require "uri"
|
6
|
+
require "rodauth/oauth"
|
7
|
+
|
8
|
+
module Rodauth
|
9
|
+
Feature.define(:oauth_tls_client_auth, :OauthTlsClientAuth) do
|
10
|
+
depends :oauth_jwt_base
|
11
|
+
|
12
|
+
auth_value_method :oauth_tls_client_certificate_bound_access_tokens, false
|
13
|
+
|
14
|
+
%i[
|
15
|
+
tls_client_auth_subject_dn tls_client_auth_san_dns
|
16
|
+
tls_client_auth_san_uri tls_client_auth_san_ip
|
17
|
+
tls_client_auth_san_email tls_client_certificate_bound_access_tokens
|
18
|
+
].each do |column|
|
19
|
+
auth_value_method :"oauth_applications_#{column}_column", column
|
20
|
+
end
|
21
|
+
|
22
|
+
auth_value_method :oauth_grants_certificate_thumbprint_column, :certificate_thumbprint
|
23
|
+
|
24
|
+
def oauth_token_endpoint_auth_methods_supported
|
25
|
+
super | %w[tls_client_auth self_signed_tls_client_auth]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_token_params
|
31
|
+
# For all requests to the authorization server utilizing mutual-TLS client authentication,
|
32
|
+
# the client MUST include the client_id parameter
|
33
|
+
redirect_response_error("invalid_request") if client_certificate && !param_or_nil("client_id")
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def require_oauth_application
|
39
|
+
return super unless client_certificate
|
40
|
+
|
41
|
+
authorization_required unless oauth_application
|
42
|
+
|
43
|
+
if supports_auth_method?(oauth_application, "tls_client_auth")
|
44
|
+
# It relies on a validated certificate chain [RFC5280]
|
45
|
+
|
46
|
+
ssl_verify = request.env["SSL_CLIENT_VERIFY"] || request.env["HTTP_SSL_CLIENT_VERIFY"] || request.env["HTTP_X_SSL_CLIENT_VERIFY"]
|
47
|
+
|
48
|
+
authorization_required unless ssl_verify == "SUCCESS"
|
49
|
+
|
50
|
+
# and a single subject distinguished name (DN) or a single subject alternative name (SAN) to
|
51
|
+
# authenticate the client. Only one subject name value of any type is used for each client.
|
52
|
+
|
53
|
+
name_matches = if oauth_application[:tls_client_auth_subject_dn]
|
54
|
+
distinguished_name_match?(client_certificate.subject, oauth_application[:tls_client_auth_subject_dn])
|
55
|
+
elsif (dns = oauth_application[:tls_client_auth_san_dns])
|
56
|
+
client_certificate_sans.any? { |san| san.tag == 2 && OpenSSL::SSL.verify_hostname(dns, san.value) }
|
57
|
+
elsif (uri = oauth_application[:tls_client_auth_san_uri])
|
58
|
+
uri = URI(uri)
|
59
|
+
client_certificate_sans.any? { |san| san.tag == 6 && URI(san.value) == uri }
|
60
|
+
elsif (ip = oauth_application[:tls_client_auth_san_ip])
|
61
|
+
ip = IPAddr.new(ip).hton
|
62
|
+
client_certificate_sans.any? { |san| san.tag == 7 && san.value == ip }
|
63
|
+
elsif (email = oauth_application[:tls_client_auth_san_email])
|
64
|
+
client_certificate_sans.any? { |san| san.tag == 1 && san.value == email }
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
authorization_required unless name_matches
|
69
|
+
|
70
|
+
oauth_application
|
71
|
+
elsif supports_auth_method?(oauth_application, "self_signed_tls_client_auth")
|
72
|
+
jwks = oauth_application_jwks(oauth_application)
|
73
|
+
|
74
|
+
thumbprint = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
|
75
|
+
|
76
|
+
# The client is successfully authenticated if the certificate that it presented during the handshake
|
77
|
+
# matches one of the certificates configured or registered for that particular client.
|
78
|
+
authorization_required unless jwks.any? { |jwk| Array(jwk[:x5c]).first == thumbprint }
|
79
|
+
|
80
|
+
oauth_application
|
81
|
+
else
|
82
|
+
super
|
83
|
+
end
|
84
|
+
rescue URI::InvalidURIError, IPAddr::InvalidAddressError
|
85
|
+
authorization_required
|
86
|
+
end
|
87
|
+
|
88
|
+
def store_token(grant_params, update_params = {})
|
89
|
+
return super unless client_certificate && (
|
90
|
+
oauth_tls_client_certificate_bound_access_tokens ||
|
91
|
+
oauth_application[oauth_applications_tls_client_certificate_bound_access_tokens_column]
|
92
|
+
)
|
93
|
+
|
94
|
+
update_params[oauth_grants_certificate_thumbprint_column] = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
def jwt_claims(oauth_grant)
|
99
|
+
claims = super
|
100
|
+
|
101
|
+
return claims unless oauth_grant[oauth_grants_certificate_thumbprint_column]
|
102
|
+
|
103
|
+
claims[:cnf] = {
|
104
|
+
"x5t#S256" => oauth_grant[oauth_grants_certificate_thumbprint_column]
|
105
|
+
}
|
106
|
+
|
107
|
+
claims
|
108
|
+
end
|
109
|
+
|
110
|
+
def json_token_introspect_payload(grant_or_claims)
|
111
|
+
claims = super
|
112
|
+
|
113
|
+
return claims unless grant_or_claims && grant_or_claims[oauth_grants_certificate_thumbprint_column]
|
114
|
+
|
115
|
+
claims[:cnf] = {
|
116
|
+
"x5t#S256" => grant_or_claims[oauth_grants_certificate_thumbprint_column]
|
117
|
+
}
|
118
|
+
|
119
|
+
claims
|
120
|
+
end
|
121
|
+
|
122
|
+
def oauth_server_metadata_body(*)
|
123
|
+
super.tap do |data|
|
124
|
+
data[:tls_client_certificate_bound_access_tokens] = oauth_tls_client_certificate_bound_access_tokens
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def client_certificate
|
129
|
+
return @client_certificate if defined?(@client_certificate)
|
130
|
+
|
131
|
+
unless (pem_cert = request.env["SSL_CLIENT_CERT"] || request.env["HTTP_SSL_CLIENT_CERT"] || request.env["HTTP_X_SSL_CLIENT_CERT"])
|
132
|
+
return
|
133
|
+
end
|
134
|
+
|
135
|
+
return if pem_cert.empty?
|
136
|
+
|
137
|
+
@certificate = OpenSSL::X509::Certificate.new(pem_cert)
|
138
|
+
end
|
139
|
+
|
140
|
+
def client_certificate_sans
|
141
|
+
return @client_certificate_sans if defined?(@client_certificate_sans)
|
142
|
+
|
143
|
+
@client_certificate_sans = begin
|
144
|
+
return [] unless client_certificate
|
145
|
+
|
146
|
+
san = client_certificate.extensions.find { |ext| ext.oid == "subjectAltName" }
|
147
|
+
|
148
|
+
return [] unless san
|
149
|
+
|
150
|
+
ostr = OpenSSL::ASN1.decode(san.to_der).value.last
|
151
|
+
|
152
|
+
sans = OpenSSL::ASN1.decode(ostr.value)
|
153
|
+
|
154
|
+
return [] unless sans
|
155
|
+
|
156
|
+
sans.value
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def distinguished_name_match?(sub1, sub2)
|
161
|
+
sub1 = OpenSSL::X509::Name.parse(sub1) if sub1.is_a?(String)
|
162
|
+
sub2 = OpenSSL::X509::Name.parse(sub2) if sub2.is_a?(String)
|
163
|
+
# OpenSSL::X509::Name#cp calls X509_NAME_cmp via openssl.
|
164
|
+
# https://www.openssl.org/docs/manmaster/man3/X509_NAME_cmp.html
|
165
|
+
# This procedure adheres to the matching rules for Distinguished Names (DN) given in
|
166
|
+
# RFC 4517 section 4.2.15 and RFC 5280 section 7.1.
|
167
|
+
sub1.cmp(sub2).zero?
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -63,12 +63,12 @@ module Rodauth
|
|
63
63
|
id_token_signing_alg_values_supported
|
64
64
|
].freeze
|
65
65
|
|
66
|
-
depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant
|
66
|
+
depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant, :oauth_implicit_grant
|
67
67
|
|
68
68
|
auth_value_method :oauth_application_scopes, %w[openid]
|
69
69
|
|
70
70
|
%i[
|
71
|
-
subject_type application_type sector_identifier_uri
|
71
|
+
subject_type application_type sector_identifier_uri initiate_login_uri
|
72
72
|
id_token_signed_response_alg id_token_encrypted_response_alg id_token_encrypted_response_enc
|
73
73
|
userinfo_signed_response_alg userinfo_encrypted_response_alg userinfo_encrypted_response_enc
|
74
74
|
].each do |column|
|
@@ -89,9 +89,16 @@ module Rodauth
|
|
89
89
|
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
|
90
90
|
|
91
91
|
auth_value_methods(
|
92
|
+
:userinfo_signing_alg_values_supported,
|
93
|
+
:userinfo_encryption_alg_values_supported,
|
94
|
+
:userinfo_encryption_enc_values_supported,
|
95
|
+
:request_object_signing_alg_values_supported,
|
96
|
+
:request_object_encryption_alg_values_supported,
|
97
|
+
:request_object_encryption_enc_values_supported,
|
92
98
|
:oauth_acr_values_supported,
|
93
99
|
:get_oidc_account_last_login_at,
|
94
100
|
:oidc_authorize_on_prompt_none?,
|
101
|
+
:fill_with_account_claims,
|
95
102
|
:get_oidc_param,
|
96
103
|
:get_additional_param,
|
97
104
|
:require_acr_value_phr,
|
@@ -112,7 +119,7 @@ module Rodauth
|
|
112
119
|
|
113
120
|
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
|
114
121
|
|
115
|
-
account =
|
122
|
+
account = account_ds(claims["sub"]).first
|
116
123
|
|
117
124
|
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
|
118
125
|
|
@@ -126,7 +133,7 @@ module Rodauth
|
|
126
133
|
|
127
134
|
oauth_grant = valid_oauth_grant_ds(
|
128
135
|
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
|
129
|
-
|
136
|
+
**resource_owner_params_from_jwt_claims(claims)
|
130
137
|
).first
|
131
138
|
|
132
139
|
claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
|
@@ -233,6 +240,30 @@ module Rodauth
|
|
233
240
|
end
|
234
241
|
end
|
235
242
|
|
243
|
+
def userinfo_signing_alg_values_supported
|
244
|
+
oauth_jwt_jws_algorithms_supported
|
245
|
+
end
|
246
|
+
|
247
|
+
def userinfo_encryption_alg_values_supported
|
248
|
+
oauth_jwt_jwe_algorithms_supported
|
249
|
+
end
|
250
|
+
|
251
|
+
def userinfo_encryption_enc_values_supported
|
252
|
+
oauth_jwt_jwe_encryption_methods_supported
|
253
|
+
end
|
254
|
+
|
255
|
+
def request_object_signing_alg_values_supported
|
256
|
+
oauth_jwt_jws_algorithms_supported
|
257
|
+
end
|
258
|
+
|
259
|
+
def request_object_encryption_alg_values_supported
|
260
|
+
oauth_jwt_jwe_algorithms_supported
|
261
|
+
end
|
262
|
+
|
263
|
+
def request_object_encryption_enc_values_supported
|
264
|
+
oauth_jwt_jwe_encryption_methods_supported
|
265
|
+
end
|
266
|
+
|
236
267
|
def oauth_acr_values_supported
|
237
268
|
acr_values = []
|
238
269
|
acr_values << "phrh" if features.include?(:webauthn_login)
|
@@ -274,29 +305,33 @@ module Rodauth
|
|
274
305
|
|
275
306
|
sc = scopes
|
276
307
|
|
277
|
-
|
278
|
-
|
308
|
+
# MUST ensure that the prompt parameter contains consent
|
309
|
+
# MUST ignore the offline_access request unless the Client
|
310
|
+
# is using a response_type value that would result in an
|
311
|
+
# Authorization Code
|
312
|
+
if sc && sc.include?("offline_access") && !(param_or_nil("prompt") == "consent" && (
|
313
|
+
(response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
|
314
|
+
))
|
279
315
|
sc.delete("offline_access")
|
280
316
|
|
281
|
-
# MUST ensure that the prompt parameter contains consent
|
282
|
-
# MUST ignore the offline_access request unless the Client
|
283
|
-
# is using a response_type value that would result in an
|
284
|
-
# Authorization Code
|
285
|
-
if param_or_nil("prompt") == "consent" && (
|
286
|
-
(response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
|
287
|
-
)
|
288
|
-
request.params["access_type"] = "offline"
|
289
|
-
end
|
290
|
-
|
291
317
|
request.params["scope"] = sc.join(" ")
|
292
318
|
end
|
293
319
|
|
294
320
|
super
|
295
321
|
|
296
|
-
|
297
|
-
|
322
|
+
response_type = param_or_nil("response_type")
|
323
|
+
|
324
|
+
is_id_token_response_type = response_type.include?("id_token")
|
325
|
+
|
326
|
+
redirect_response_error("invalid_request") if is_id_token_response_type && !param_or_nil("nonce")
|
327
|
+
|
328
|
+
return unless is_id_token_response_type || response_type == "code token"
|
329
|
+
|
330
|
+
response_mode = param_or_nil("response_mode")
|
331
|
+
|
332
|
+
# id_token: The default Response Mode for this Response Type is the fragment encoding and the query encoding MUST NOT be used.
|
298
333
|
|
299
|
-
redirect_response_error("invalid_request") unless
|
334
|
+
redirect_response_error("invalid_request") unless response_mode.nil? || response_mode == "fragment"
|
300
335
|
end
|
301
336
|
|
302
337
|
def require_authorizable_account
|
@@ -333,8 +368,9 @@ module Rodauth
|
|
333
368
|
|
334
369
|
identifier_uri = URI(identifier_uri).host
|
335
370
|
|
336
|
-
|
337
|
-
|
371
|
+
account_ids = oauth_grant.values_at(oauth_grants_resource_owner_columns)
|
372
|
+
values = [identifier_uri, *account_ids, oauth_jwt_subject_secret]
|
373
|
+
Digest::SHA256.hexdigest(values.join)
|
338
374
|
else
|
339
375
|
raise StandardError, "unexpected subject (#{subject_type})"
|
340
376
|
end
|
@@ -434,8 +470,8 @@ module Rodauth
|
|
434
470
|
end
|
435
471
|
|
436
472
|
def create_oauth_grant_with_token(create_params = {})
|
473
|
+
create_params.merge!(resource_owner_params)
|
437
474
|
create_params[oauth_grants_type_column] = "hybrid"
|
438
|
-
create_params[oauth_grants_account_id_column] = account_id
|
439
475
|
create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in)
|
440
476
|
authorization_code = create_oauth_grant(create_params)
|
441
477
|
access_token = if oauth_jwt_access_tokens
|
@@ -462,24 +498,7 @@ module Rodauth
|
|
462
498
|
signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
|
463
499
|
oauth_jwt_keys.keys.first
|
464
500
|
|
465
|
-
|
466
|
-
|
467
|
-
id_token_claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
|
468
|
-
|
469
|
-
id_token_claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
|
470
|
-
|
471
|
-
# Time when the End-User authentication occurred.
|
472
|
-
id_token_claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
|
473
|
-
|
474
|
-
# Access Token hash value.
|
475
|
-
if (access_token = oauth_grant[oauth_grants_token_column])
|
476
|
-
id_token_claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
|
477
|
-
end
|
478
|
-
|
479
|
-
# code hash value.
|
480
|
-
if (code = oauth_grant[oauth_grants_code_column])
|
481
|
-
id_token_claims[:c_hash] = id_token_hash(code, signing_algorithm)
|
482
|
-
end
|
501
|
+
id_claims = id_token_claims(oauth_grant, signing_algorithm)
|
483
502
|
|
484
503
|
account = db[accounts_table].where(account_id_column => oauth_grant[oauth_grants_account_id_column]).first
|
485
504
|
|
@@ -499,7 +518,7 @@ module Rodauth
|
|
499
518
|
|
500
519
|
# 5.4 - However, when no Access Token is issued (which is the case for the response_type value id_token),
|
501
520
|
# the resulting Claims are returned in the ID Token.
|
502
|
-
fill_with_account_claims(
|
521
|
+
fill_with_account_claims(id_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
|
503
522
|
|
504
523
|
params = {
|
505
524
|
jwks: oauth_application_jwks(oauth_application),
|
@@ -508,7 +527,30 @@ module Rodauth
|
|
508
527
|
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
|
509
528
|
}.compact
|
510
529
|
|
511
|
-
oauth_grant[:id_token] = jwt_encode(
|
530
|
+
oauth_grant[:id_token] = jwt_encode(id_claims, **params)
|
531
|
+
end
|
532
|
+
|
533
|
+
def id_token_claims(oauth_grant, signing_algorithm)
|
534
|
+
claims = jwt_claims(oauth_grant)
|
535
|
+
|
536
|
+
claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
|
537
|
+
|
538
|
+
claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
|
539
|
+
|
540
|
+
# Time when the End-User authentication occurred.
|
541
|
+
claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
|
542
|
+
|
543
|
+
# Access Token hash value.
|
544
|
+
if (access_token = oauth_grant[oauth_grants_token_column])
|
545
|
+
claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
|
546
|
+
end
|
547
|
+
|
548
|
+
# code hash value.
|
549
|
+
if (code = oauth_grant[oauth_grants_code_column])
|
550
|
+
claims[:c_hash] = id_token_hash(code, signing_algorithm)
|
551
|
+
end
|
552
|
+
|
553
|
+
claims
|
512
554
|
end
|
513
555
|
|
514
556
|
# aka fill_with_standard_claims
|
@@ -587,14 +629,14 @@ module Rodauth
|
|
587
629
|
additional_info = additional_claims_info[param] || EMPTY_HASH
|
588
630
|
value = additional_info["value"] || meth[account, param]
|
589
631
|
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
590
|
-
cl[param] = value
|
632
|
+
cl[param] = value unless value.nil?
|
591
633
|
end
|
592
634
|
elsif claims_locales.nil?
|
593
635
|
lambda do |account, param, cl = claims|
|
594
636
|
additional_info = additional_claims_info[param] || EMPTY_HASH
|
595
637
|
value = additional_info["value"] || meth[account, param, nil]
|
596
638
|
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
597
|
-
cl[param] = value
|
639
|
+
cl[param] = value unless value.nil?
|
598
640
|
end
|
599
641
|
else
|
600
642
|
lambda do |account, param, cl = claims|
|
@@ -626,10 +668,9 @@ module Rodauth
|
|
626
668
|
|
627
669
|
def check_valid_response_type?
|
628
670
|
case param_or_nil("response_type")
|
629
|
-
when "none", "id_token", "code id_token" # multiple
|
671
|
+
when "none", "id_token", "code id_token", # multiple
|
672
|
+
"code token", "id_token token", "code id_token token"
|
630
673
|
true
|
631
|
-
when "code token", "id_token token", "code id_token token"
|
632
|
-
supports_token_response_type?
|
633
674
|
else
|
634
675
|
super
|
635
676
|
end
|
@@ -641,10 +682,6 @@ module Rodauth
|
|
641
682
|
param("response_type") == "none"
|
642
683
|
end
|
643
684
|
|
644
|
-
def supports_token_response_type?
|
645
|
-
features.include?(:oauth_implicit_grant)
|
646
|
-
end
|
647
|
-
|
648
685
|
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
649
686
|
response_type = param("response_type")
|
650
687
|
case response_type
|
@@ -653,8 +690,6 @@ module Rodauth
|
|
653
690
|
generate_id_token(grant_params, true)
|
654
691
|
response_params.replace("id_token" => grant_params[:id_token])
|
655
692
|
when "code token"
|
656
|
-
redirect_response_error("invalid_request") unless supports_token_response_type?
|
657
|
-
|
658
693
|
response_params.replace(create_oauth_grant_with_token)
|
659
694
|
when "code id_token"
|
660
695
|
params = _do_authorize_code
|
@@ -665,16 +700,12 @@ module Rodauth
|
|
665
700
|
"code" => params["code"]
|
666
701
|
)
|
667
702
|
when "id_token token"
|
668
|
-
redirect_response_error("invalid_request") unless supports_token_response_type?
|
669
|
-
|
670
703
|
grant_params = oidc_grant_params.merge(oauth_grants_type_column => "hybrid")
|
671
704
|
oauth_grant = _do_authorize_token(grant_params)
|
672
705
|
generate_id_token(oauth_grant)
|
673
706
|
|
674
707
|
response_params.replace(json_access_token_payload(oauth_grant))
|
675
708
|
when "code id_token token"
|
676
|
-
redirect_response_error("invalid_request") unless supports_token_response_type?
|
677
|
-
|
678
709
|
params = create_oauth_grant_with_token
|
679
710
|
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
|
680
711
|
oauth_grant[oauth_grants_token_column] = params["access_token"]
|
@@ -691,9 +722,10 @@ module Rodauth
|
|
691
722
|
|
692
723
|
def oidc_grant_params
|
693
724
|
grant_params = {
|
694
|
-
|
725
|
+
**resource_owner_params,
|
695
726
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
696
|
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
727
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
728
|
+
oauth_grants_redirect_uri_column => param_or_nil("redirect_uri")
|
697
729
|
}
|
698
730
|
if (nonce = param_or_nil("nonce"))
|
699
731
|
grant_params[oauth_grants_nonce_column] = nonce
|
@@ -708,6 +740,12 @@ module Rodauth
|
|
708
740
|
grant_params
|
709
741
|
end
|
710
742
|
|
743
|
+
def generate_token(grant_params = {}, should_generate_refresh_token = true)
|
744
|
+
scopes = grant_params[oauth_grants_scopes_column].split(oauth_scope_separator)
|
745
|
+
|
746
|
+
super(grant_params, scopes.include?("offline_access") && should_generate_refresh_token)
|
747
|
+
end
|
748
|
+
|
711
749
|
def authorize_response(params, mode)
|
712
750
|
redirect_url = URI.parse(redirect_uri)
|
713
751
|
redirect(redirect_url.to_s) if mode == "none"
|
@@ -10,7 +10,7 @@ module Rodauth
|
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
|
-
def validate_client_registration_params
|
13
|
+
def validate_client_registration_params(*)
|
14
14
|
super
|
15
15
|
|
16
16
|
if (value = @oauth_application_params[oauth_applications_application_type_column])
|
@@ -43,7 +43,11 @@ module Rodauth
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column])
|
46
|
+
if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column]) && !check_valid_uri?(value)
|
47
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
|
48
|
+
end
|
49
|
+
|
50
|
+
if (value = @oauth_application_params[oauth_applications_initiate_login_uri_column])
|
47
51
|
uri = URI(value)
|
48
52
|
|
49
53
|
unless uri.scheme == "https" || uri.host == "localhost"
|
@@ -170,6 +174,44 @@ module Rodauth
|
|
170
174
|
register_throw_json_response_error("invalid_client_metadata",
|
171
175
|
register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
|
172
176
|
end
|
177
|
+
|
178
|
+
if features.include?(:oauth_jwt_secured_authorization_response_mode)
|
179
|
+
if defined?(oauth_applications_authorization_signed_response_alg_column) &&
|
180
|
+
(value = @oauth_application_params[oauth_applications_authorization_signed_response_alg_column]) &&
|
181
|
+
(!oauth_jwt_jws_algorithms_supported.include?(value) || value == "none")
|
182
|
+
register_throw_json_response_error("invalid_client_metadata",
|
183
|
+
register_invalid_client_metadata_message("authorization_signed_response_alg", value))
|
184
|
+
end
|
185
|
+
|
186
|
+
if defined?(oauth_applications_authorization_encrypted_response_alg_column) &&
|
187
|
+
(value = @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]) &&
|
188
|
+
!oauth_jwt_jwe_algorithms_supported.include?(value)
|
189
|
+
register_throw_json_response_error("invalid_client_metadata",
|
190
|
+
register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
|
191
|
+
end
|
192
|
+
|
193
|
+
if defined?(oauth_applications_authorization_encrypted_response_enc_column)
|
194
|
+
if (value = @oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column])
|
195
|
+
|
196
|
+
unless @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
|
197
|
+
# When authorization_encrypted_response_enc is included, authorization_encrypted_response_alg MUST also be provided.
|
198
|
+
register_throw_json_response_error("invalid_client_metadata",
|
199
|
+
register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
|
204
|
+
register_throw_json_response_error("invalid_client_metadata",
|
205
|
+
register_invalid_client_metadata_message("authorization_encrypted_response_enc", value))
|
206
|
+
end
|
207
|
+
elsif @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
|
208
|
+
# If authorization_encrypted_response_alg is specified, the default for this value is A128CBC-HS256.
|
209
|
+
@oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column] = "A128CBC-HS256"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
@oauth_application_params
|
173
215
|
end
|
174
216
|
|
175
217
|
def validate_client_registration_response_type(response_type, grant_types)
|
@@ -219,5 +261,13 @@ module Rodauth
|
|
219
261
|
def register_invalid_application_type_message(application_type)
|
220
262
|
"The application type '#{application_type}' is not allowed."
|
221
263
|
end
|
264
|
+
|
265
|
+
def initialize_register_params(create_params, return_params)
|
266
|
+
super
|
267
|
+
registration_access_token = oauth_unique_id_generator
|
268
|
+
create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
|
269
|
+
return_params["registration_access_token"] = registration_access_token
|
270
|
+
return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
|
271
|
+
end
|
222
272
|
end
|
223
273
|
end
|