rodauth-oauth 1.0.0.pre.beta1 → 1.0.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/CHANGELOG.md +1 -1
- data/MIGRATION-GUIDE-v1.md +12 -0
- data/README.md +30 -15
- data/doc/release_notes/0_1_0.md +2 -2
- data/doc/release_notes/0_2_0.md +1 -1
- data/doc/release_notes/0_3_0.md +1 -1
- data/doc/release_notes/0_5_0.md +2 -2
- data/doc/release_notes/0_8_0.md +2 -2
- data/doc/release_notes/1_0_0.md +79 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +19 -7
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize_error.erb +10 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +54 -43
- data/lib/rodauth/features/oauth_application_management.rb +2 -2
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +31 -7
- data/lib/rodauth/features/oauth_authorize_base.rb +32 -10
- data/lib/rodauth/features/oauth_base.rb +36 -16
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +7 -4
- data/lib/rodauth/features/oauth_implicit_grant.rb +16 -5
- data/lib/rodauth/features/oauth_jwt.rb +3 -3
- data/lib/rodauth/features/oauth_jwt_base.rb +29 -6
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +7 -4
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +64 -10
- data/lib/rodauth/features/oauth_resource_indicators.rb +0 -4
- data/lib/rodauth/features/oauth_resource_server.rb +3 -3
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +2 -0
- data/lib/rodauth/features/oidc.rb +263 -187
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +65 -25
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +118 -0
- data/lib/rodauth/oauth/http_extensions.rb +15 -2
- data/lib/rodauth/oauth/ttl_store.rb +2 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +4 -1
- data/locales/pt.yml +4 -1
- data/templates/authorize.str +17 -10
- data/templates/authorize_error.str +12 -0
- metadata +15 -12
- data/doc/release_notes/1_0_0_beta1.md +0 -38
@@ -17,6 +17,7 @@ module Rodauth
|
|
17
17
|
issuer
|
18
18
|
authorization_endpoint
|
19
19
|
end_session_endpoint
|
20
|
+
backchannel_logout_session_supported
|
20
21
|
token_endpoint
|
21
22
|
userinfo_endpoint
|
22
23
|
jwks_uri
|
@@ -74,10 +75,9 @@ module Rodauth
|
|
74
75
|
auth_value_method :"oauth_applications_#{column}_column", column
|
75
76
|
end
|
76
77
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
auth_value_method :oauth_grants_acr_column, :acr
|
78
|
+
%i[nonce acr claims_locales claims].each do |column|
|
79
|
+
auth_value_method :"oauth_grants_#{column}_column", column
|
80
|
+
end
|
81
81
|
|
82
82
|
auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
|
83
83
|
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
@@ -88,12 +88,10 @@ module Rodauth
|
|
88
88
|
auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
|
89
89
|
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
|
90
90
|
|
91
|
-
# logout
|
92
|
-
auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
|
93
|
-
auth_value_method :use_rp_initiated_logout?, false
|
94
|
-
|
95
91
|
auth_value_methods(
|
92
|
+
:oauth_acr_values_supported,
|
96
93
|
:get_oidc_account_last_login_at,
|
94
|
+
:oidc_authorize_on_prompt_none?,
|
97
95
|
:get_oidc_param,
|
98
96
|
:get_additional_param,
|
99
97
|
:require_acr_value_phr,
|
@@ -122,11 +120,28 @@ module Rodauth
|
|
122
120
|
|
123
121
|
oidc_claims = { "sub" => claims["sub"] }
|
124
122
|
|
125
|
-
fill_with_account_claims(oidc_claims, account, oauth_scopes)
|
126
|
-
|
127
123
|
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
128
124
|
|
129
|
-
|
125
|
+
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless @oauth_application
|
126
|
+
|
127
|
+
oauth_grant = valid_oauth_grant_ds(
|
128
|
+
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
|
129
|
+
oauth_grants_account_id_column => account[account_id_column]
|
130
|
+
).first
|
131
|
+
|
132
|
+
claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
|
133
|
+
|
134
|
+
if (claims = oauth_grant[oauth_grants_claims_column])
|
135
|
+
claims = JSON.parse(claims)
|
136
|
+
if (userinfo_essential_claims = claims["userinfo"])
|
137
|
+
oauth_scopes |= userinfo_essential_claims.to_a
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# 5.4 - The Claims requested by the profile, email, address, and phone scope values are returned from the UserInfo Endpoint
|
142
|
+
fill_with_account_claims(oidc_claims, account, oauth_scopes, claims_locales)
|
143
|
+
|
144
|
+
if (algo = @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
|
130
145
|
params = {
|
131
146
|
jwks: oauth_application_jwks(@oauth_application),
|
132
147
|
encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
|
@@ -134,7 +149,12 @@ module Rodauth
|
|
134
149
|
}.compact
|
135
150
|
|
136
151
|
jwt = jwt_encode(
|
137
|
-
oidc_claims
|
152
|
+
oidc_claims.merge(
|
153
|
+
# If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value
|
154
|
+
# SHOULD be the OP's Issuer Identifier URL. The aud value SHOULD be or include the RP's Client ID value.
|
155
|
+
iss: oauth_jwt_issuer,
|
156
|
+
aud: @oauth_application[oauth_applications_client_id_column]
|
157
|
+
),
|
138
158
|
signing_algorithm: algo,
|
139
159
|
**params
|
140
160
|
)
|
@@ -148,81 +168,6 @@ module Rodauth
|
|
148
168
|
end
|
149
169
|
end
|
150
170
|
|
151
|
-
# /oidc-logout
|
152
|
-
auth_server_route(:oidc_logout) do |r|
|
153
|
-
next unless use_rp_initiated_logout?
|
154
|
-
|
155
|
-
require_authorizable_account
|
156
|
-
before_oidc_logout_route
|
157
|
-
|
158
|
-
# OpenID Providers MUST support the use of the HTTP GET and POST methods
|
159
|
-
r.on method: %i[get post] do
|
160
|
-
catch_error do
|
161
|
-
validate_oidc_logout_params
|
162
|
-
|
163
|
-
#
|
164
|
-
# why this is done:
|
165
|
-
#
|
166
|
-
# we need to decode the id token in order to get the application, because, if the
|
167
|
-
# signing key is application-specific, we don't know how to verify the signature
|
168
|
-
# beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
|
169
|
-
# the @oauth_application, and then decode-and-verify.
|
170
|
-
#
|
171
|
-
claims = jwt_decode(param("id_token_hint"), verify_claims: false)
|
172
|
-
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
173
|
-
oauth_grant = db[oauth_grants_table]
|
174
|
-
.where(
|
175
|
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
176
|
-
oauth_grants_account_id_column => account_id
|
177
|
-
).first
|
178
|
-
|
179
|
-
# check whether ID token belongs to currently logged-in user
|
180
|
-
redirect_response_error("invalid_request") unless oauth_grant && claims["sub"] == jwt_subject(
|
181
|
-
oauth_grant, oauth_application
|
182
|
-
)
|
183
|
-
|
184
|
-
# When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
|
185
|
-
redirect_response_error("invalid_request") unless claims && claims["iss"] == oauth_jwt_issuer
|
186
|
-
|
187
|
-
# now let's logout from IdP
|
188
|
-
transaction do
|
189
|
-
before_logout
|
190
|
-
logout
|
191
|
-
after_logout
|
192
|
-
end
|
193
|
-
|
194
|
-
if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
|
195
|
-
catch(:default_logout_redirect) do
|
196
|
-
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
197
|
-
|
198
|
-
throw(:default_logout_redirect) unless oauth_application
|
199
|
-
|
200
|
-
post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uri_column].split(" ")
|
201
|
-
|
202
|
-
throw(:default_logout_redirect) unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
|
203
|
-
|
204
|
-
if (state = param_or_nil("state"))
|
205
|
-
post_logout_redirect_uri = URI(post_logout_redirect_uri)
|
206
|
-
params = ["state=#{state}"]
|
207
|
-
params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
|
208
|
-
post_logout_redirect_uri.query = params.join("&")
|
209
|
-
post_logout_redirect_uri = post_logout_redirect_uri.to_s
|
210
|
-
end
|
211
|
-
|
212
|
-
redirect(post_logout_redirect_uri)
|
213
|
-
end
|
214
|
-
|
215
|
-
end
|
216
|
-
|
217
|
-
# regular logout procedure
|
218
|
-
set_notice_flash(logout_notice_flash)
|
219
|
-
redirect(logout_redirect)
|
220
|
-
end
|
221
|
-
|
222
|
-
redirect_response_error("invalid_request")
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
171
|
def load_openid_configuration_route(alt_issuer = nil)
|
227
172
|
request.on(".well-known/openid-configuration") do
|
228
173
|
allow_cors(request)
|
@@ -260,7 +205,11 @@ module Rodauth
|
|
260
205
|
end
|
261
206
|
|
262
207
|
def oauth_response_types_supported
|
263
|
-
|
208
|
+
grant_types = oauth_grant_types_supported
|
209
|
+
oidc_response_types = %w[id_token none]
|
210
|
+
oidc_response_types |= ["code id_token"] if grant_types.include?("authorization_code")
|
211
|
+
oidc_response_types |= ["code token", "id_token token", "code id_token token"] if grant_types.include?("implicit")
|
212
|
+
super | oidc_response_types
|
264
213
|
end
|
265
214
|
|
266
215
|
def current_oauth_account
|
@@ -284,27 +233,76 @@ module Rodauth
|
|
284
233
|
end
|
285
234
|
end
|
286
235
|
|
236
|
+
def oauth_acr_values_supported
|
237
|
+
acr_values = []
|
238
|
+
acr_values << "phrh" if features.include?(:webauthn_login)
|
239
|
+
acr_values << "phr" if respond_to?(:require_two_factor_authenticated)
|
240
|
+
acr_values
|
241
|
+
end
|
242
|
+
|
243
|
+
def oidc_authorize_on_prompt_none?(_account)
|
244
|
+
false
|
245
|
+
end
|
246
|
+
|
287
247
|
def validate_authorize_params
|
288
|
-
|
248
|
+
if (max_age = param_or_nil("max_age"))
|
289
249
|
|
290
|
-
|
250
|
+
max_age = Integer(max_age)
|
291
251
|
|
292
|
-
|
252
|
+
redirect_response_error("invalid_request") unless max_age.positive?
|
293
253
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
254
|
+
if Time.now - get_oidc_account_last_login_at(session_value) > max_age
|
255
|
+
# force user to re-login
|
256
|
+
clear_session
|
257
|
+
set_session_value(login_redirect_session_key, request.fullpath)
|
258
|
+
redirect require_login_redirect
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
if (claims = param_or_nil("claims"))
|
263
|
+
# The value is a JSON object listing the requested Claims.
|
264
|
+
claims = JSON.parse(claims)
|
265
|
+
|
266
|
+
claims.each do |_, individual_claims|
|
267
|
+
redirect_response_error("invalid_request") unless individual_claims.is_a?(Hash)
|
268
|
+
|
269
|
+
individual_claims.each do |_, claim|
|
270
|
+
redirect_response_error("invalid_request") unless claim.nil? || individual_claims.is_a?(Hash)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
sc = scopes
|
276
|
+
|
277
|
+
if sc && sc.include?("offline_access")
|
278
|
+
|
279
|
+
sc.delete("offline_access")
|
280
|
+
|
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
|
+
request.params["scope"] = sc.join(" ")
|
299
292
|
end
|
300
293
|
|
301
294
|
super
|
295
|
+
|
296
|
+
return unless (response_type = param_or_nil("response_type"))
|
297
|
+
return unless response_type.include?("id_token")
|
298
|
+
|
299
|
+
redirect_response_error("invalid_request") unless param_or_nil("nonce")
|
302
300
|
end
|
303
301
|
|
304
302
|
def require_authorizable_account
|
305
303
|
try_prompt
|
306
304
|
super
|
307
|
-
try_acr_values
|
305
|
+
@acr = try_acr_values
|
308
306
|
end
|
309
307
|
|
310
308
|
def get_oidc_account_last_login_at(account_id)
|
@@ -348,22 +346,18 @@ module Rodauth
|
|
348
346
|
|
349
347
|
case prompt
|
350
348
|
when "none"
|
349
|
+
return unless request.get?
|
350
|
+
|
351
351
|
redirect_response_error("login_required") unless logged_in?
|
352
352
|
|
353
353
|
require_account
|
354
354
|
|
355
|
-
|
356
|
-
oauth_grants_account_id_column => account_id,
|
357
|
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
358
|
-
oauth_grants_redirect_uri_column => redirect_uri,
|
359
|
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
360
|
-
oauth_grants_access_type_column => "online"
|
361
|
-
).count.zero?
|
362
|
-
redirect_response_error("consent_required")
|
363
|
-
end
|
355
|
+
redirect_response_error("interaction_required") unless oidc_authorize_on_prompt_none?(account_from_session)
|
364
356
|
|
365
357
|
request.env["REQUEST_METHOD"] = "POST"
|
366
358
|
when "login"
|
359
|
+
return unless request.get?
|
360
|
+
|
367
361
|
if logged_in? && request.cookies[oauth_prompt_login_cookie_key] == "login"
|
368
362
|
::Rack::Utils.delete_cookie_header!(response.headers, oauth_prompt_login_cookie_key, oauth_prompt_login_cookie_options)
|
369
363
|
return
|
@@ -380,18 +374,17 @@ module Rodauth
|
|
380
374
|
|
381
375
|
redirect require_login_redirect
|
382
376
|
when "consent"
|
377
|
+
return unless request.post?
|
378
|
+
|
383
379
|
require_account
|
384
380
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
390
|
-
oauth_grants_access_type_column => "online"
|
391
|
-
).count.zero?
|
392
|
-
redirect_response_error("consent_required")
|
393
|
-
end
|
381
|
+
sc = scopes || []
|
382
|
+
|
383
|
+
redirect_response_error("consent_required") if sc.empty?
|
384
|
+
|
394
385
|
when "select-account"
|
386
|
+
return unless request.get?
|
387
|
+
|
395
388
|
# only works if select_account plugin is available
|
396
389
|
require_select_account if respond_to?(:require_select_account)
|
397
390
|
else
|
@@ -403,48 +396,72 @@ module Rodauth
|
|
403
396
|
return unless (acr_values = param_or_nil("acr_values"))
|
404
397
|
|
405
398
|
acr_values.split(" ").each do |acr_value|
|
399
|
+
next unless oauth_acr_values_supported.include?(acr_value)
|
400
|
+
|
406
401
|
case acr_value
|
407
|
-
when "phr"
|
408
|
-
|
402
|
+
when "phr"
|
403
|
+
return acr_value if require_acr_value_phr
|
404
|
+
when "phrh"
|
405
|
+
return acr_value if require_acr_value_phrh
|
409
406
|
else
|
410
|
-
require_acr_value(acr_value)
|
407
|
+
return acr_value if require_acr_value(acr_value)
|
411
408
|
end
|
412
409
|
end
|
410
|
+
|
411
|
+
nil
|
413
412
|
end
|
414
413
|
|
415
414
|
def require_acr_value_phr
|
416
|
-
return unless respond_to?(:require_two_factor_authenticated)
|
415
|
+
return false unless respond_to?(:require_two_factor_authenticated)
|
417
416
|
|
418
417
|
require_two_factor_authenticated
|
418
|
+
true
|
419
419
|
end
|
420
420
|
|
421
421
|
def require_acr_value_phrh
|
422
|
+
return false unless features.include?(:webauthn_login)
|
423
|
+
|
422
424
|
require_acr_value_phr && two_factor_login_type_match?("webauthn")
|
423
425
|
end
|
424
426
|
|
425
|
-
def require_acr_value(_acr)
|
427
|
+
def require_acr_value(_acr)
|
428
|
+
true
|
429
|
+
end
|
426
430
|
|
427
431
|
def create_oauth_grant(create_params = {})
|
428
|
-
|
429
|
-
create_params[oauth_grants_nonce_column] = nonce
|
430
|
-
end
|
431
|
-
if (acr = param_or_nil("acr"))
|
432
|
-
create_params[oauth_grants_acr_column] = acr
|
433
|
-
end
|
432
|
+
create_params.replace(oidc_grant_params.merge(create_params))
|
434
433
|
super
|
435
434
|
end
|
436
435
|
|
436
|
+
def create_oauth_grant_with_token(create_params = {})
|
437
|
+
create_params[oauth_grants_type_column] = "hybrid"
|
438
|
+
create_params[oauth_grants_account_id_column] = account_id
|
439
|
+
create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in)
|
440
|
+
authorization_code = create_oauth_grant(create_params)
|
441
|
+
access_token = if oauth_jwt_access_tokens
|
442
|
+
_generate_jwt_access_token(create_params)
|
443
|
+
else
|
444
|
+
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => authorization_code).first
|
445
|
+
_generate_access_token(oauth_grant)
|
446
|
+
end
|
447
|
+
|
448
|
+
json_access_token_payload(oauth_grants_token_column => access_token).merge("code" => authorization_code)
|
449
|
+
end
|
450
|
+
|
437
451
|
def create_token(*)
|
438
452
|
oauth_grant = super
|
439
453
|
generate_id_token(oauth_grant)
|
440
454
|
oauth_grant
|
441
455
|
end
|
442
456
|
|
443
|
-
def generate_id_token(oauth_grant)
|
457
|
+
def generate_id_token(oauth_grant, include_claims = false)
|
444
458
|
oauth_scopes = oauth_grant[oauth_grants_scopes_column].split(oauth_scope_separator)
|
445
459
|
|
446
460
|
return unless oauth_scopes.include?("openid")
|
447
461
|
|
462
|
+
signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
|
463
|
+
oauth_jwt_keys.keys.first
|
464
|
+
|
448
465
|
id_token_claims = jwt_claims(oauth_grant)
|
449
466
|
|
450
467
|
id_token_claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
|
@@ -454,6 +471,16 @@ module Rodauth
|
|
454
471
|
# Time when the End-User authentication occurred.
|
455
472
|
id_token_claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
|
456
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
|
483
|
+
|
457
484
|
account = db[accounts_table].where(account_id_column => oauth_grant[oauth_grants_account_id_column]).first
|
458
485
|
|
459
486
|
# this should never happen!
|
@@ -461,14 +488,22 @@ module Rodauth
|
|
461
488
|
# who just authorized its generation.
|
462
489
|
return unless account
|
463
490
|
|
464
|
-
|
491
|
+
if (claims = oauth_grant[oauth_grants_claims_column])
|
492
|
+
claims = JSON.parse(claims)
|
493
|
+
if (id_token_essential_claims = claims["id_token"])
|
494
|
+
oauth_scopes |= id_token_essential_claims.to_a
|
495
|
+
|
496
|
+
include_claims = true
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
# 5.4 - However, when no Access Token is issued (which is the case for the response_type value id_token),
|
501
|
+
# the resulting Claims are returned in the ID Token.
|
502
|
+
fill_with_account_claims(id_token_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
|
465
503
|
|
466
504
|
params = {
|
467
505
|
jwks: oauth_application_jwks(oauth_application),
|
468
|
-
signing_algorithm:
|
469
|
-
oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
|
470
|
-
oauth_jwt_keys.keys.first
|
471
|
-
),
|
506
|
+
signing_algorithm: signing_algorithm,
|
472
507
|
encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
|
473
508
|
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
|
474
509
|
}.compact
|
@@ -477,11 +512,31 @@ module Rodauth
|
|
477
512
|
end
|
478
513
|
|
479
514
|
# aka fill_with_standard_claims
|
480
|
-
def fill_with_account_claims(claims, account, scopes)
|
515
|
+
def fill_with_account_claims(claims, account, scopes, claims_locales)
|
516
|
+
additional_claims_info = {}
|
517
|
+
|
481
518
|
scopes_by_claim = scopes.each_with_object({}) do |scope, by_oidc|
|
482
519
|
next if scope == "openid"
|
483
520
|
|
484
|
-
|
521
|
+
if scope.is_a?(Array)
|
522
|
+
# essential claims
|
523
|
+
param, additional_info = scope
|
524
|
+
|
525
|
+
param = param.to_sym
|
526
|
+
|
527
|
+
oidc, = OIDC_SCOPES_MAP.find do |_, oidc_scopes|
|
528
|
+
oidc_scopes.include?(param)
|
529
|
+
end || param.to_s
|
530
|
+
|
531
|
+
param = nil if oidc == param.to_s
|
532
|
+
|
533
|
+
additional_claims_info[param] = additional_info
|
534
|
+
else
|
535
|
+
|
536
|
+
oidc, param = scope.split(".", 2)
|
537
|
+
|
538
|
+
param = param.to_sym if param
|
539
|
+
end
|
485
540
|
|
486
541
|
by_oidc[oidc] ||= []
|
487
542
|
|
@@ -490,13 +545,11 @@ module Rodauth
|
|
490
545
|
|
491
546
|
oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
|
492
547
|
|
493
|
-
|
494
|
-
claims_locales = claims_locales.split(" ").map(&:to_sym)
|
495
|
-
end
|
548
|
+
claims_locales = claims_locales.split(" ").map(&:to_sym) if claims_locales
|
496
549
|
|
497
550
|
unless oidc_scopes.empty?
|
498
551
|
if respond_to?(:get_oidc_param)
|
499
|
-
get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales)
|
552
|
+
get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales, additional_claims_info)
|
500
553
|
|
501
554
|
oidc_scopes.each do |scope|
|
502
555
|
scope_claims = claims
|
@@ -517,7 +570,7 @@ module Rodauth
|
|
517
570
|
return if additional_scopes.empty?
|
518
571
|
|
519
572
|
if respond_to?(:get_additional_param)
|
520
|
-
get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales)
|
573
|
+
get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales, additional_claims_info)
|
521
574
|
|
522
575
|
additional_scopes.each do |scope|
|
523
576
|
get_additional_param[account, scope.to_sym]
|
@@ -527,23 +580,36 @@ module Rodauth
|
|
527
580
|
end
|
528
581
|
end
|
529
582
|
|
530
|
-
def proxy_get_param(get_param_func, claims, claims_locales)
|
583
|
+
def proxy_get_param(get_param_func, claims, claims_locales, additional_claims_info)
|
531
584
|
meth = method(get_param_func)
|
532
585
|
if meth.arity == 2
|
533
|
-
|
586
|
+
lambda do |account, param, cl = claims|
|
587
|
+
additional_info = additional_claims_info[param] || EMPTY_HASH
|
588
|
+
value = additional_info["value"] || meth[account, param]
|
589
|
+
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
590
|
+
cl[param] = value if value
|
591
|
+
end
|
534
592
|
elsif claims_locales.nil?
|
535
|
-
|
593
|
+
lambda do |account, param, cl = claims|
|
594
|
+
additional_info = additional_claims_info[param] || EMPTY_HASH
|
595
|
+
value = additional_info["value"] || meth[account, param, nil]
|
596
|
+
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
597
|
+
cl[param] = value if value
|
598
|
+
end
|
536
599
|
else
|
537
600
|
lambda do |account, param, cl = claims|
|
538
601
|
claims_values = claims_locales.map do |locale|
|
539
|
-
|
540
|
-
|
602
|
+
additional_info = additional_claims_info[param] || EMPTY_HASH
|
603
|
+
value = additional_info["value"] || meth[account, param, locale]
|
604
|
+
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
605
|
+
value
|
606
|
+
end.compact
|
541
607
|
|
542
608
|
if claims_values.uniq.size == 1
|
543
609
|
cl[param] = claims_values.first
|
544
610
|
else
|
545
611
|
claims_locales.zip(claims_values).each do |locale, value|
|
546
|
-
cl["#{param}##{locale}"] = value
|
612
|
+
cl["#{param}##{locale}"] = value if value
|
547
613
|
end
|
548
614
|
end
|
549
615
|
end
|
@@ -580,23 +646,41 @@ module Rodauth
|
|
580
646
|
end
|
581
647
|
|
582
648
|
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
583
|
-
|
649
|
+
response_type = param("response_type")
|
650
|
+
case response_type
|
584
651
|
when "id_token"
|
585
|
-
|
652
|
+
grant_params = oidc_grant_params
|
653
|
+
generate_id_token(grant_params, true)
|
654
|
+
response_params.replace("id_token" => grant_params[:id_token])
|
586
655
|
when "code token"
|
587
656
|
redirect_response_error("invalid_request") unless supports_token_response_type?
|
588
657
|
|
589
|
-
response_params.replace(
|
658
|
+
response_params.replace(create_oauth_grant_with_token)
|
590
659
|
when "code id_token"
|
591
|
-
|
660
|
+
params = _do_authorize_code
|
661
|
+
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
|
662
|
+
generate_id_token(oauth_grant)
|
663
|
+
response_params.replace(
|
664
|
+
"id_token" => oauth_grant[:id_token],
|
665
|
+
"code" => params["code"]
|
666
|
+
)
|
592
667
|
when "id_token token"
|
593
668
|
redirect_response_error("invalid_request") unless supports_token_response_type?
|
594
669
|
|
595
|
-
|
670
|
+
grant_params = oidc_grant_params.merge(oauth_grants_type_column => "hybrid")
|
671
|
+
oauth_grant = _do_authorize_token(grant_params)
|
672
|
+
generate_id_token(oauth_grant)
|
673
|
+
|
674
|
+
response_params.replace(json_access_token_payload(oauth_grant))
|
596
675
|
when "code id_token token"
|
597
676
|
redirect_response_error("invalid_request") unless supports_token_response_type?
|
598
677
|
|
599
|
-
|
678
|
+
params = create_oauth_grant_with_token
|
679
|
+
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
|
680
|
+
oauth_grant[oauth_grants_token_column] = params["access_token"]
|
681
|
+
generate_id_token(oauth_grant)
|
682
|
+
|
683
|
+
response_params.replace(params.merge("id_token" => oauth_grant[:id_token]))
|
600
684
|
when "none"
|
601
685
|
response_mode ||= "none"
|
602
686
|
end
|
@@ -605,23 +689,23 @@ module Rodauth
|
|
605
689
|
super(response_params, response_mode)
|
606
690
|
end
|
607
691
|
|
608
|
-
def
|
692
|
+
def oidc_grant_params
|
609
693
|
grant_params = {
|
610
694
|
oauth_grants_account_id_column => account_id,
|
611
695
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
612
|
-
oauth_grants_scopes_column => scopes.join(
|
696
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
613
697
|
}
|
614
698
|
if (nonce = param_or_nil("nonce"))
|
615
699
|
grant_params[oauth_grants_nonce_column] = nonce
|
616
700
|
end
|
617
|
-
|
618
|
-
|
701
|
+
grant_params[oauth_grants_acr_column] = @acr if @acr
|
702
|
+
if (claims_locales = param_or_nil("claims_locales"))
|
703
|
+
grant_params[oauth_grants_claims_locales_column] = claims_locales
|
619
704
|
end
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
params
|
705
|
+
if (claims = param_or_nil("claims"))
|
706
|
+
grant_params[oauth_grants_claims_column] = claims
|
707
|
+
end
|
708
|
+
grant_params
|
625
709
|
end
|
626
710
|
|
627
711
|
def authorize_response(params, mode)
|
@@ -630,18 +714,6 @@ module Rodauth
|
|
630
714
|
super
|
631
715
|
end
|
632
716
|
|
633
|
-
# Logout
|
634
|
-
|
635
|
-
def validate_oidc_logout_params
|
636
|
-
redirect_response_error("invalid_request") unless param_or_nil("id_token_hint")
|
637
|
-
# check if valid token hint type
|
638
|
-
return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
|
639
|
-
|
640
|
-
return if check_valid_uri?(redirect_uri)
|
641
|
-
|
642
|
-
redirect_response_error("invalid_request")
|
643
|
-
end
|
644
|
-
|
645
717
|
# Webfinger
|
646
718
|
|
647
719
|
def json_webfinger_payload
|
@@ -673,25 +745,15 @@ module Rodauth
|
|
673
745
|
|
674
746
|
scope_claims.unshift("auth_time")
|
675
747
|
|
676
|
-
response_types_supported = metadata[:response_types_supported]
|
677
|
-
|
678
|
-
response_types_supported |= %w[none]
|
679
|
-
response_types_supported |= ["code id_token"] if metadata[:grant_types_supported].include?("authorization_code")
|
680
|
-
if metadata[:grant_types_supported].include?("implicit")
|
681
|
-
response_types_supported |= ["code token", "id_token token", "code id_token token"]
|
682
|
-
end
|
683
|
-
|
684
|
-
alg_values, enc_values = oauth_jwt_jwe_keys.keys.transpose
|
685
|
-
|
686
748
|
metadata.merge(
|
687
749
|
userinfo_endpoint: userinfo_url,
|
688
|
-
end_session_endpoint: (oidc_logout_url if use_rp_initiated_logout?),
|
689
|
-
response_types_supported: response_types_supported,
|
690
750
|
subject_types_supported: %w[public pairwise],
|
751
|
+
acr_values_supported: oauth_acr_values_supported,
|
752
|
+
claims_parameter_supported: true,
|
691
753
|
|
692
|
-
id_token_signing_alg_values_supported:
|
693
|
-
id_token_encryption_alg_values_supported:
|
694
|
-
id_token_encryption_enc_values_supported:
|
754
|
+
id_token_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
|
755
|
+
id_token_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
|
756
|
+
id_token_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
|
695
757
|
|
696
758
|
userinfo_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
|
697
759
|
userinfo_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
|
@@ -737,5 +799,19 @@ module Rodauth
|
|
737
799
|
end
|
738
800
|
return_response(jwt)
|
739
801
|
end
|
802
|
+
|
803
|
+
def id_token_hash(hash, algo)
|
804
|
+
digest = case algo
|
805
|
+
when /256/ then Digest::SHA256
|
806
|
+
when /384/ then Digest::SHA384
|
807
|
+
when /512/ then Digest::SHA512
|
808
|
+
end
|
809
|
+
|
810
|
+
return unless digest
|
811
|
+
|
812
|
+
hash = digest.digest(hash)
|
813
|
+
hash = hash[0...hash.size / 2]
|
814
|
+
Base64.urlsafe_encode64(hash).tr("=", "")
|
815
|
+
end
|
740
816
|
end
|
741
817
|
end
|