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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +4 -4
- data/docs/api.md +8 -5
- data/docs/authentication-claims.md +11 -1
- data/docs/ciba-conformance.md +58 -0
- data/docs/ciba-core-requirements.json +3537 -0
- data/docs/claims.md +5 -1
- data/docs/max-age.md +3 -1
- data/docs/operations.md +28 -1
- data/docs/pairwise.md +20 -4
- data/docs/refresh-tokens.md +32 -0
- data/docs/release-readiness.md +22 -3
- data/docs/release-validation.md +5 -0
- data/docs/requirements-audit-current.json +2 -1
- data/docs/requirements-audit-current.md +2 -0
- data/examples/extensions_smoke.rb +18 -1
- data/lib/rodauth/ciba/pairwise.rb +7 -3
- data/lib/rodauth/ciba/registration.rb +4 -0
- data/lib/rodauth/ciba/version.rb +1 -1
- data/lib/rodauth/features/ciba.rb +195 -19
- metadata +4 -2
|
@@ -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:
|
|
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
|
-
|
|
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 && (
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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] &&
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
+
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
|