rodauth-ciba 0.1.0 → 0.2.1

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.
@@ -140,6 +140,10 @@ module Rodauth
140
140
  def oauth_application_jwks(application)
141
141
  previous = @ciba_jwks_cache_lookup
142
142
  @ciba_jwks_cache_lookup = @ciba_request_key_lookup || @ciba_verifying_client_assertion || @ciba_tls_key_lookup || @ciba_authentication
143
+ jwks_uri = application[oauth_applications_jwks_uri_column]
144
+ if @ciba_jwks_cache_lookup && jwks_uri && !ciba_https_uri?(jwks_uri)
145
+ raise CibaSupport::HTTP::Error, "CIBA key URI must use HTTPS"
146
+ end
143
147
  super
144
148
  ensure
145
149
  @ciba_jwks_cache_lookup = previous
@@ -151,6 +155,7 @@ module Rodauth
151
155
 
152
156
  def http_request_with_cache(uri, *args)
153
157
  return super unless @ciba_jwks_cache_lookup
158
+ raise CibaSupport::HTTP::Error, "CIBA key URI must use HTTPS" unless ciba_https_uri?(uri.to_s)
154
159
  http_request_cache.fetch(URI(uri.to_s)) { http_request(uri, *args) }
155
160
  rescue CibaSupport::JwksCache::StatusError
156
161
  authorization_required
@@ -159,6 +164,7 @@ module Rodauth
159
164
  def http_request(uri, form_data = nil, &block)
160
165
  return super unless @ciba_request_key_lookup || @ciba_verifying_client_assertion || @ciba_tls_key_lookup || @ciba_authentication
161
166
  raise CibaSupport::HTTP::Error, "unexpected JWKS request body" if form_data
167
+ raise CibaSupport::HTTP::Error, "CIBA key URI must use HTTPS" unless ciba_https_uri?(uri.to_s)
162
168
  uri = URI(uri.to_s)
163
169
  outgoing = Net::HTTP::Get.new(uri.request_uri)
164
170
  outgoing["accept"] = json_response_content_type
@@ -185,7 +191,11 @@ module Rodauth
185
191
  !request.params.key?("client_assertion")
186
192
  authorization_required
187
193
  end
188
- super
194
+ super.tap do
195
+ # Bind identity and recipient keys to the metadata that actually
196
+ # authenticated this request, before route hooks or DCR can run.
197
+ @ciba_authenticated_application ||= ciba_snapshot(oauth_application)
198
+ end
189
199
  end
190
200
  end
191
201
 
@@ -366,6 +376,14 @@ module Rodauth
366
376
  Rack::Request.new(env).url
367
377
  end
368
378
 
379
+ def ciba_https_uri?(value)
380
+ return false unless value.is_a?(String) && !value.empty?
381
+ uri = URI(value)
382
+ uri.scheme == "https" && uri.host && !uri.userinfo && !uri.fragment
383
+ rescue URI::InvalidURIError
384
+ false
385
+ end
386
+
369
387
  def ciba_validate_authorization_details(details, oauth_application:)
370
388
  raise CibaSupport::ConfigurationError, "configure authorization-details validation"
371
389
  end
@@ -419,6 +437,7 @@ module Rodauth
419
437
  auth_server_route(:backchannel_authentication, "backchannel-authentication") do |r|
420
438
  r.post do
421
439
  ciba_http do
440
+ received_at = ciba_now
422
441
  validate_ciba_transport!
423
442
  require_oauth_application
424
443
  ciba_error("unauthorized_client") unless ciba_client_eligible?(oauth_application)
@@ -444,10 +463,9 @@ module Rodauth
444
463
  public_id = SecureRandom.urlsafe_base64(32)
445
464
  delivery[:auth_req_id] = public_id if delivery
446
465
  lifetime = requested.delete(:lifetime)
447
- now = ciba_now
448
466
  row = requested.merge(auth_req_id_digest: Digest::SHA256.hexdigest(public_id),
449
467
  account_id: account_id, oauth_application_id: oauth_application[oauth_applications_id_column],
450
- created_at: now, expires_at: now + lifetime,
468
+ created_at: received_at, expires_at: received_at + lifetime,
451
469
  status: "pending", lock_version: 0, interval: ciba_poll_interval)
452
470
  ciba_transaction do
453
471
  @ciba_current_request = ciba_snapshot(row)
@@ -895,6 +913,16 @@ module Rodauth
895
913
 
896
914
  def jwt_encode(payload, signing_algorithm: oauth_jwt_keys.keys.first, headers: {}, **options)
897
915
  ciba_id_token = @ciba_authentication && headers[:typ] == "id_token+jwt"
916
+ if ciba_id_token
917
+ identity = payload.each_with_object({}) do |(name, value), result|
918
+ key = name.to_s
919
+ result[key] = value if %w[iss sub aud auth_time].include?(key)
920
+ end
921
+ if @ciba_refresh_identity && identity != @ciba_refresh_identity
922
+ raise CibaSupport::ProtocolError, "invalid_grant"
923
+ end
924
+ @ciba_initial_id_token = identity
925
+ end
898
926
  if ciba_id_token && ciba_id_token_encryption_enabled &&
899
927
  (options[:encryption_algorithm] || options[:encryption_method])
900
928
  signed = jwt_encode(payload, signing_algorithm: signing_algorithm, headers: headers,
@@ -1039,7 +1067,7 @@ module Rodauth
1039
1067
  raise CibaSupport::IdentityMismatch, "account does not match" unless row[:account_id] == account_id
1040
1068
  raise CibaSupport::Expired, "request expired" if row[:expires_at] <= ciba_now
1041
1069
  if outcome == "approved" && row[:authentication_claims] &&
1042
- JSON.parse(row[:authentication_claims]).include?("auth_time_required") && context[:auth_time].nil?
1070
+ ciba_authentication_claim_selection(row).include?("auth_time_required") && context[:auth_time].nil?
1043
1071
  raise CibaSupport::Ineligible, "verified auth_time is required for this CIBA request"
1044
1072
  end
1045
1073
  authorized_scopes = context[:grant_id] ? ciba_authorized_scopes!(row, context[:grant_id]) : row[:scopes].split
@@ -1054,6 +1082,9 @@ module Rodauth
1054
1082
  unless ciba_account_eligible?(account_id) && ciba_client_eligible?(application, authorized_scopes)
1055
1083
  raise CibaSupport::Ineligible, "account or client unavailable"
1056
1084
  end
1085
+ if outcome == "approved" && !ciba_authentication_constraints_valid?(row.merge(context), application)
1086
+ raise CibaSupport::Ineligible, "authentication does not satisfy requested claims"
1087
+ end
1057
1088
  ciba_claim!(row)
1058
1089
  action = outcome == "approved" ? "approve" : "deny"
1059
1090
  @ciba_current_request = ciba_snapshot(row)
@@ -1381,7 +1412,7 @@ module Rodauth
1381
1412
  next unless data.key?(target)
1382
1413
  entries = data[target]
1383
1414
  unless entries.is_a?(Hash) && entries.size <= 64 && entries.all? { |name, options|
1384
- name.bytesize <= 128 && (options.nil? || options.is_a?(Hash))
1415
+ name.bytesize <= 128 && ciba_claim_request_options_valid?(name, options)
1385
1416
  }
1386
1417
  ciba_error("invalid_request")
1387
1418
  end
@@ -1398,7 +1429,7 @@ module Rodauth
1398
1429
  allowed = ciba_claim_names(consent.fetch("allowed"))
1399
1430
  rejected = ciba_claim_names(consent.fetch("rejected"))
1400
1431
  implicit = scopes.flat_map { |scope| scope == "address" ? ["address"] : Array(OIDC_SCOPES_MAP[scope]).map(&:to_s) }
1401
- result = {"version" => 1}
1432
+ result = {"version" => 1, "constraints" => requested}
1402
1433
  %w[id_token userinfo].each do |target|
1403
1434
  names = Array(requested[target]&.keys) & allowed
1404
1435
  names |= implicit if target == "userinfo"
@@ -1451,6 +1482,86 @@ module Rodauth
1451
1482
  claims[name.to_sym] = value unless value.nil?
1452
1483
  end
1453
1484
  end
1485
+ if target == "userinfo" && row
1486
+ claims["sub"] = row[ciba_token_subject_column] || jwt_subject(account[account_id_column], oauth_application)
1487
+ end
1488
+ if target == "userinfo" && oauth_application[oauth_applications_userinfo_signed_response_alg_column]
1489
+ # Upstream adds these after this hook. Validate the same final values.
1490
+ claims[:iss] = oauth_jwt_issuer
1491
+ claims[:aud] = oauth_application[oauth_applications_client_id_column]
1492
+ end
1493
+ ciba_filter_requested_claims!(claims, projection.fetch("constraints", {}).fetch(target, {}))
1494
+ rescue CibaSupport::ProtocolError
1495
+ raise unless target == "userinfo"
1496
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_token")
1497
+ end
1498
+
1499
+ def ciba_claim_value_matches?(value, options)
1500
+ return true unless options.is_a?(Hash) && (options.key?("value") || options.key?("values"))
1501
+ # Compare JSON values, not Ruby symbol/string object-key representations.
1502
+ value = JSON.parse(JSON.generate(value))
1503
+ (!options.key?("value") || options["value"] == value) &&
1504
+ (!options.key?("values") || (options["values"].is_a?(Array) && options["values"].include?(value)))
1505
+ end
1506
+
1507
+ def ciba_claim_request_options_valid?(name, options)
1508
+ return true if options.nil?
1509
+ return false unless options.is_a?(Hash) &&
1510
+ (!options.key?("essential") || options["essential"] == true || options["essential"] == false) &&
1511
+ (!options.key?("values") || options["values"].is_a?(Array))
1512
+ values = options.key?("value") ? [options["value"]] : []
1513
+ values += options["values"] if options.key?("values")
1514
+ values.all? do |value|
1515
+ case name
1516
+ when "auth_time", "iat", "exp", "updated_at"
1517
+ value.is_a?(Numeric) && value.finite? && value >= 0
1518
+ when "email_verified", "phone_number_verified"
1519
+ value == true || value == false
1520
+ when "address"
1521
+ value.is_a?(Hash)
1522
+ when "amr"
1523
+ value.is_a?(Array) && value.all? { |entry| entry.is_a?(String) }
1524
+ else
1525
+ string_claims = OIDC_SCOPES_MAP.values.flatten.map(&:to_s) + %w[sub iss acr nonce]
1526
+ !string_claims.include?(name) || value.is_a?(String)
1527
+ end
1528
+ end
1529
+ end
1530
+
1531
+ def ciba_authentication_constraints_valid?(row, application)
1532
+ requested = JSON.parse(row[:requested_claims] || "{}")
1533
+ requested.each do |target, entries|
1534
+ next unless %w[id_token userinfo].include?(target)
1535
+ return false unless entries.is_a?(Hash) && entries.all? { |name, options| ciba_claim_request_options_valid?(name, options) }
1536
+ subject = row[:token_digest] ? ciba_refresh_identity(row).fetch("sub") : jwt_subject(row[:account_id], application)
1537
+ if entries.key?("sub") && !ciba_claim_value_matches?(subject, entries["sub"])
1538
+ return false
1539
+ end
1540
+ if target == "userinfo" && application[oauth_applications_userinfo_signed_response_alg_column]
1541
+ return false unless ciba_claim_value_matches?(oauth_jwt_issuer, entries["iss"]) &&
1542
+ ciba_claim_value_matches?(application[oauth_applications_client_id_column], entries["aud"])
1543
+ end
1544
+ end
1545
+ acr = requested.fetch("id_token", {})["acr"]
1546
+ !acr.is_a?(Hash) || acr["essential"] != true ||
1547
+ (!acr.key?("value") && !acr.key?("values")) ||
1548
+ (!row[:acr].nil? && ciba_claim_value_matches?(row[:acr], acr))
1549
+ end
1550
+
1551
+ def ciba_filter_requested_claims!(claims, requested, auth_time_required: false)
1552
+ requested.each do |name, options|
1553
+ key = claims.key?(name) ? name : name.to_sym
1554
+ next unless claims.key?(key)
1555
+ # OIDC gives voluntary ACR its own semantics: return the achieved ACR.
1556
+ next if name == "acr" && (!options.is_a?(Hash) || options["essential"] != true)
1557
+ next if ciba_claim_value_matches?(claims[key], options)
1558
+ if %w[sub iss aud exp iat nonce].include?(name) || (name == "auth_time" && auth_time_required) ||
1559
+ (name == "acr" && options.is_a?(Hash) && options["essential"] == true)
1560
+ raise CibaSupport::ProtocolError, "invalid_grant"
1561
+ end
1562
+ claims.delete(key)
1563
+ end
1564
+ claims
1454
1565
  end
1455
1566
 
1456
1567
  def ciba_account_claim_value(account, name, getter)
@@ -1581,6 +1692,9 @@ module Rodauth
1581
1692
  end
1582
1693
  if claims.key?("requested_expiry")
1583
1694
  value = claims["requested_expiry"]
1695
+ if value.is_a?(Numeric) && value.finite? && value > 0 && value == value.to_i
1696
+ value = value.to_i
1697
+ end
1584
1698
  ciba_error("invalid_request") unless value.is_a?(String) || value.is_a?(Integer)
1585
1699
  parameters["requested_expiry"] = value.to_s
1586
1700
  end
@@ -1777,11 +1891,15 @@ module Rodauth
1777
1891
  !result[:max_age].nil?
1778
1892
  authentication_claims << "auth_time"
1779
1893
  end
1780
- authentication_claims << "auth_time_required" if oauth_application[oauth_applications_require_auth_time_column] == true
1894
+ auth_time_required = oauth_application[oauth_applications_require_auth_time_column] == true || !result[:max_age].nil?
1781
1895
  authentication_claims << "acr" if acr
1782
1896
  if result[:requested_claims]
1783
- authentication_claims |= JSON.parse(result[:requested_claims]).fetch("id_token", {}).keys & %w[auth_time acr amr]
1897
+ id_token_claims = JSON.parse(result[:requested_claims]).fetch("id_token", {})
1898
+ authentication_claims |= id_token_claims.keys & %w[auth_time acr amr]
1899
+ auth_time_request = id_token_claims["auth_time"]
1900
+ auth_time_required ||= auth_time_request.is_a?(Hash) && auth_time_request["essential"] == true
1784
1901
  end
1902
+ authentication_claims << "auth_time_required" if auth_time_required
1785
1903
  result[:authentication_claims] = JSON.generate(authentication_claims)
1786
1904
  result[:requested_resources] = ciba_requested_resources if ciba_resources_enabled
1787
1905
  if ciba_authorization_details_enabled
@@ -1837,7 +1955,7 @@ module Rodauth
1837
1955
  unless row[:status] == "approved" && ciba_account_eligible?(row[:account_id]) && ciba_client_eligible?(application, authorized_scopes)
1838
1956
  raise CibaSupport::ProtocolError, "invalid_grant"
1839
1957
  end
1840
- @oauth_application = application
1958
+ @oauth_application = @ciba_authenticated_application
1841
1959
  @ciba_current_request = ciba_snapshot(row)
1842
1960
  before_ciba_issue
1843
1961
  raise CibaSupport::Expired, "request expired" if row[:expires_at] <= ciba_now
@@ -1846,13 +1964,16 @@ module Rodauth
1846
1964
  rescue CibaSupport::Ineligible, CibaSupport::IdentityMismatch, ArgumentError
1847
1965
  raise CibaSupport::ProtocolError, "invalid_grant"
1848
1966
  end
1967
+ unless ciba_account_eligible?(row[:account_id]) && ciba_client_eligible?(application, authorized_scopes)
1968
+ raise CibaSupport::ProtocolError, "invalid_grant"
1969
+ end
1849
1970
  ciba_validate_enabled_capabilities!(row)
1850
1971
  updates = {status: "consumed", consumed_at: ciba_now}
1851
1972
  ciba_ds.where(ciba_values(id: row[:id])).update(ciba_values(updates))
1852
1973
  grant = ciba_generate_access_token(row, authorized_scopes)
1853
1974
  if ciba_refresh_tokens_enabled && authorized_scopes.include?("offline_access") &&
1854
1975
  application[oauth_applications_grant_types_column].to_s.split.include?("refresh_token")
1855
- grant[oauth_grants_refresh_token_column] = ciba_save_refresh_token(row)
1976
+ grant[oauth_grants_refresh_token_column] = ciba_save_refresh_token(row, initial_id_token: @ciba_initial_id_token)
1856
1977
  end
1857
1978
  row.merge!(updates)
1858
1979
  @ciba_current_request = ciba_snapshot(row)
@@ -1862,7 +1983,7 @@ module Rodauth
1862
1983
  end
1863
1984
  ensure
1864
1985
  if grant_type == CibaSupport::GRANT_TYPE
1865
- @ciba_authentication = @ciba_current_request = nil
1986
+ @ciba_authentication = @ciba_current_request = @ciba_initial_id_token = @ciba_refresh_identity = nil
1866
1987
  end
1867
1988
  end
1868
1989
 
@@ -1946,11 +2067,46 @@ module Rodauth
1946
2067
  .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1947
2068
  end
1948
2069
 
1949
- def ciba_save_refresh_token(source, rotations: 0, created_at: ciba_now)
2070
+ def ciba_authentication_claim_selection(row)
2071
+ value = JSON.parse(row[:authentication_claims] || '["auth_time","acr","amr"]')
2072
+ selected = value.is_a?(Hash) && value["version"] == 2 ? value["selected"] : value
2073
+ unless selected.is_a?(Array) && selected.all? { |name| name.is_a?(String) }
2074
+ raise CibaSupport::ProtocolError, "invalid_grant"
2075
+ end
2076
+ selected
2077
+ rescue JSON::ParserError, TypeError
2078
+ raise CibaSupport::ProtocolError, "invalid_grant"
2079
+ end
2080
+
2081
+ def ciba_refresh_identity(row)
2082
+ value = JSON.parse(row[:authentication_claims] || "null")
2083
+ identity = value.is_a?(Hash) && value["version"] == 2 && value["initial_id_token"]
2084
+ unless identity.is_a?(Hash) && %w[iss sub aud].all? { |key| identity[key].is_a?(String) && !identity[key].empty? } &&
2085
+ identity["sub"].ascii_only? && identity["sub"].bytesize <= 255 &&
2086
+ (identity.keys - %w[iss sub aud auth_time]).empty? &&
2087
+ (!identity.key?("auth_time") || (identity["auth_time"].is_a?(Numeric) && identity["auth_time"].finite? && identity["auth_time"] >= 0))
2088
+ raise CibaSupport::ProtocolError, "invalid_grant"
2089
+ end
2090
+ selected = ciba_authentication_claim_selection(row)
2091
+ if (selected.include?("auth_time_required") && !identity.key?("auth_time")) ||
2092
+ (identity.key?("auth_time") && identity["auth_time"] != row[:auth_time])
2093
+ raise CibaSupport::ProtocolError, "invalid_grant"
2094
+ end
2095
+ identity
2096
+ rescue JSON::ParserError, TypeError
2097
+ raise CibaSupport::ProtocolError, "invalid_grant"
2098
+ end
2099
+
2100
+ def ciba_save_refresh_token(source, rotations: 0, created_at: ciba_now, initial_id_token: nil)
1950
2101
  token = "ciba_rt_#{SecureRandom.urlsafe_base64(32)}"
1951
2102
  keys = %i[grant_id account_id oauth_application_id scopes auth_time acr amr nonce authentication_claims
1952
2103
  requested_claims requested_resources requested_authorization_details]
1953
2104
  row = source.select { |key, _| keys.include?(key) }
2105
+ if initial_id_token
2106
+ row[:authentication_claims] = JSON.generate(version: 2, selected: ciba_authentication_claim_selection(source),
2107
+ initial_id_token: initial_id_token)
2108
+ end
2109
+ ciba_refresh_identity(row)
1954
2110
  row.merge!(token_digest: Digest::SHA256.hexdigest(token), created_at: created_at,
1955
2111
  issued_at: ciba_now, expires_at: ciba_now + ciba_refresh_token_lifetime, rotations: rotations)
1956
2112
  db[ciba_refresh_tokens_table].insert(row)
@@ -1969,7 +2125,7 @@ module Rodauth
1969
2125
  ciba_error("invalid_grant") unless row && row[:expires_at] > ciba_now
1970
2126
  ciba_guard([:refresh, row[:id]]) { ciba_refresh_source(row, ds, token) }
1971
2127
  ensure
1972
- @ciba_authentication = @ciba_current_refresh = nil
2128
+ @ciba_authentication = @ciba_current_refresh = @ciba_initial_id_token = @ciba_refresh_identity = nil
1973
2129
  end
1974
2130
 
1975
2131
  def ciba_validate_refresh_source!(row)
@@ -1986,7 +2142,16 @@ module Rodauth
1986
2142
  unless scopes.include?("offline_access") && ciba_account_eligible?(row[:account_id]) && ciba_client_eligible?(application, scopes)
1987
2143
  raise CibaSupport::ProtocolError, "invalid_grant"
1988
2144
  end
1989
- @oauth_application = application
2145
+ @oauth_application = @ciba_authenticated_application
2146
+ identity = ciba_refresh_identity(row)
2147
+ unless identity["iss"] == oauth_jwt_issuer && identity["aud"] == application[oauth_applications_client_id_column] &&
2148
+ (!identity.key?("auth_time") || identity["auth_time"] == row[:auth_time])
2149
+ raise CibaSupport::ProtocolError, "invalid_grant"
2150
+ end
2151
+ unless (application[oauth_applications_subject_type_column] || oauth_jwt_subject_type) == "pairwise" ||
2152
+ identity["sub"] == jwt_subject(row[:account_id], application)
2153
+ raise CibaSupport::ProtocolError, "invalid_grant"
2154
+ end
1990
2155
  ciba_validate_enabled_capabilities!(row)
1991
2156
  scopes
1992
2157
  end
@@ -2037,11 +2202,13 @@ module Rodauth
2037
2202
 
2038
2203
  def ciba_generate_access_token(row, authorized_scopes)
2039
2204
  @ciba_authentication = row
2205
+ @ciba_initial_id_token = nil
2206
+ @ciba_refresh_identity = row[:token_digest] ? ciba_refresh_identity(row) : nil
2040
2207
  attributes = {oauth_grants_account_id_column => row[:account_id],
2041
2208
  oauth_grants_oauth_application_id_column => row[:oauth_application_id],
2042
2209
  oauth_grants_scopes_column => authorized_scopes.join(oauth_scope_separator),
2043
2210
  oauth_grants_ciba_grant_id_column => row[:grant_id], oauth_grants_type_column => CibaSupport::GRANT_TYPE}
2044
- attributes[ciba_token_subject_column] = jwt_subject(row[:account_id]) if ciba_pairwise_enabled
2211
+ attributes[ciba_token_subject_column] = @ciba_refresh_identity ? @ciba_refresh_identity.fetch("sub") : jwt_subject(row[:account_id]) if ciba_pairwise_enabled
2045
2212
  attributes[:ciba_claims] = ciba_compile_claims(row, authorized_scopes) if ciba_claims_enabled
2046
2213
  resource_attributes = ciba_token_resource(row)
2047
2214
  attributes.merge!(resource_attributes) if resource_attributes
@@ -2069,7 +2236,10 @@ module Rodauth
2069
2236
  end
2070
2237
 
2071
2238
  def ciba_validate_enabled_capabilities!(row)
2072
- if row[:authentication_claims] && JSON.parse(row[:authentication_claims]).include?("auth_time_required") && row[:auth_time].nil?
2239
+ if row[:authentication_claims] && ciba_authentication_claim_selection(row).include?("auth_time_required") && row[:auth_time].nil?
2240
+ raise CibaSupport::ProtocolError, "invalid_grant"
2241
+ end
2242
+ unless ciba_authentication_constraints_valid?(row, oauth_application)
2073
2243
  raise CibaSupport::ProtocolError, "invalid_grant"
2074
2244
  end
2075
2245
  consent = ciba_locked_grant(row[:grant_id])
@@ -2098,16 +2268,22 @@ module Rodauth
2098
2268
  # ID Token audience is the RP, even when access-token resource audiences differ.
2099
2269
  claims[:aud] = oauth_application[oauth_applications_client_id_column]
2100
2270
  row = @ciba_authentication
2101
- claims[:nonce] = row[:nonce] if row[:nonce]
2102
- selected = row[:authentication_claims] ? JSON.parse(row[:authentication_claims]) : %w[auth_time acr amr]
2271
+ claims[:nonce] = row[:nonce] if row[:nonce] && !@ciba_refresh_identity
2272
+ selected = ciba_authentication_claim_selection(row)
2103
2273
  grant[oauth_grants_scopes_column].to_s.split(oauth_scope_separator).each do |scope|
2104
2274
  selected |= Array(ciba_authentication_claims_by_scope[scope]) & %w[auth_time acr amr]
2105
2275
  end
2106
2276
  claims[:auth_time] = row[:auth_time] if row[:auth_time] && selected.include?("auth_time")
2107
2277
  claims[:acr] = row[:acr] if row[:acr] && selected.include?("acr")
2108
2278
  claims[:amr] = JSON.parse(row[:amr]) if row[:amr] && selected.include?("amr")
2279
+ if @ciba_refresh_identity
2280
+ claims[:sub] = @ciba_refresh_identity.fetch("sub")
2281
+ claims.delete(:auth_time)
2282
+ claims[:auth_time] = @ciba_refresh_identity["auth_time"] if @ciba_refresh_identity.key?("auth_time")
2283
+ end
2109
2284
  claims[:at_hash] = id_token_hash(grant[oauth_grants_token_column], algorithm)
2110
- claims
2285
+ requested = JSON.parse(row[:requested_claims] || "{}").fetch("id_token", {})
2286
+ ciba_filter_requested_claims!(claims, requested, auth_time_required: selected.include?("auth_time_required"))
2111
2287
  end
2112
2288
 
2113
2289
  def ciba_emit(row, type, from, to)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-ciba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rodauth-ciba contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-11 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rodauth-oauth
@@ -125,6 +125,8 @@ files:
125
125
  - docs/api.md
126
126
  - docs/authentication-claims.md
127
127
  - docs/authorization-details.md
128
+ - docs/ciba-conformance.md
129
+ - docs/ciba-core-requirements.json
128
130
  - docs/claims.md
129
131
  - docs/failure-contract.md
130
132
  - docs/id-token-encryption.md