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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/MIGRATION-GUIDE-v1.md +12 -0
  4. data/README.md +30 -15
  5. data/doc/release_notes/0_1_0.md +2 -2
  6. data/doc/release_notes/0_2_0.md +1 -1
  7. data/doc/release_notes/0_3_0.md +1 -1
  8. data/doc/release_notes/0_5_0.md +2 -2
  9. data/doc/release_notes/0_8_0.md +2 -2
  10. data/doc/release_notes/1_0_0.md +79 -0
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +19 -7
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize_error.erb +10 -0
  13. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +54 -43
  14. data/lib/rodauth/features/oauth_application_management.rb +2 -2
  15. data/lib/rodauth/features/oauth_authorization_code_grant.rb +31 -7
  16. data/lib/rodauth/features/oauth_authorize_base.rb +32 -10
  17. data/lib/rodauth/features/oauth_base.rb +36 -16
  18. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +7 -4
  19. data/lib/rodauth/features/oauth_implicit_grant.rb +16 -5
  20. data/lib/rodauth/features/oauth_jwt.rb +3 -3
  21. data/lib/rodauth/features/oauth_jwt_base.rb +29 -6
  22. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +7 -4
  23. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +64 -10
  24. data/lib/rodauth/features/oauth_resource_indicators.rb +0 -4
  25. data/lib/rodauth/features/oauth_resource_server.rb +3 -3
  26. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +2 -0
  27. data/lib/rodauth/features/oidc.rb +263 -187
  28. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +65 -25
  29. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +118 -0
  30. data/lib/rodauth/oauth/http_extensions.rb +15 -2
  31. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  32. data/lib/rodauth/oauth/version.rb +1 -1
  33. data/locales/en.yml +4 -1
  34. data/locales/pt.yml +4 -1
  35. data/templates/authorize.str +17 -10
  36. data/templates/authorize_error.str +12 -0
  37. metadata +15 -12
  38. 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
- auth_value_method :oauth_grants_nonce_column, :nonce
78
- auth_value_method :oauth_grants_acr_column, :acr
79
- auth_value_method :oauth_grants_nonce_column, :nonce
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
- if (algo = @oauth_application && @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
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
- super | %w[id_token none]
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
- return super unless (max_age = param_or_nil("max_age"))
248
+ if (max_age = param_or_nil("max_age"))
289
249
 
290
- max_age = Integer(max_age)
250
+ max_age = Integer(max_age)
291
251
 
292
- redirect_response_error("invalid_request") unless max_age.positive?
252
+ redirect_response_error("invalid_request") unless max_age.positive?
293
253
 
294
- if Time.now - get_oidc_account_last_login_at(session_value) > max_age
295
- # force user to re-login
296
- clear_session
297
- set_session_value(login_redirect_session_key, request.fullpath)
298
- redirect require_login_redirect
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
- if db[oauth_grants_table].where(
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
- if db[oauth_grants_table].where(
386
- oauth_grants_account_id_column => account_id,
387
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
388
- oauth_grants_redirect_uri_column => redirect_uri,
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" then require_acr_value_phr
408
- when "phrh" then require_acr_value_phrh
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); end
427
+ def require_acr_value(_acr)
428
+ true
429
+ end
426
430
 
427
431
  def create_oauth_grant(create_params = {})
428
- if (nonce = param_or_nil("nonce"))
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
- fill_with_account_claims(id_token_claims, account, oauth_scopes)
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
- oidc, param = scope.split(".", 2)
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
- if (claims_locales = param_or_nil("claims_locales"))
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
- ->(account, param, cl = claims) { cl[param] = meth[account, param] }
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
- ->(account, param, cl = claims) { cl[param] = meth[account, param, nil] }
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
- meth[account, param, locale]
540
- end
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
- case param("response_type")
649
+ response_type = param("response_type")
650
+ case response_type
584
651
  when "id_token"
585
- response_params.replace(_do_authorize_id_token)
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(_do_authorize_code.merge(_do_authorize_token))
658
+ response_params.replace(create_oauth_grant_with_token)
590
659
  when "code id_token"
591
- response_params.replace(_do_authorize_code.merge(_do_authorize_id_token))
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
- response_params.replace(_do_authorize_id_token.merge(_do_authorize_token))
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
- response_params.replace(_do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token))
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 _do_authorize_id_token
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
- if (acr = param_or_nil("acr"))
618
- grant_params[oauth_grants_acr_column] = acr
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
- oauth_grant = generate_token(grant_params, false)
621
- generate_id_token(oauth_grant)
622
- params = json_access_token_payload(oauth_grant)
623
- params.delete("access_token")
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: metadata[:token_endpoint_auth_signing_alg_values_supported],
693
- id_token_encryption_alg_values_supported: Array(alg_values),
694
- id_token_encryption_enc_values_supported: Array(enc_values),
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