rodauth-ciba 0.1.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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +34 -0
  3. data/LICENSE +21 -0
  4. data/README.md +106 -0
  5. data/docs/adr/0003-follow-rodauth-hook-conventions.md +9 -0
  6. data/docs/adr/0004-retain-atomic-issuance-and-stage-alignment.md +16 -0
  7. data/docs/alignment-closeout.md +136 -0
  8. data/docs/alignment-roadmap.md +160 -0
  9. data/docs/api.md +337 -0
  10. data/docs/authentication-claims.md +80 -0
  11. data/docs/authorization-details.md +50 -0
  12. data/docs/claims.md +46 -0
  13. data/docs/failure-contract.md +34 -0
  14. data/docs/id-token-encryption.md +288 -0
  15. data/docs/id-token-hint.md +174 -0
  16. data/docs/login-hint-token.md +35 -0
  17. data/docs/max-age.md +65 -0
  18. data/docs/node-alignment.md +336 -0
  19. data/docs/operations.md +130 -0
  20. data/docs/outbound-http.md +52 -0
  21. data/docs/pairwise.md +76 -0
  22. data/docs/ping.md +118 -0
  23. data/docs/protocol-coverage.md +114 -0
  24. data/docs/refresh-tokens.md +151 -0
  25. data/docs/release-readiness.md +75 -0
  26. data/docs/release-validation.md +498 -0
  27. data/docs/request-context.md +51 -0
  28. data/docs/requirements-audit-current.json +3221 -0
  29. data/docs/requirements-audit-current.md +424 -0
  30. data/docs/requirements-audit.json +3190 -0
  31. data/docs/requirements-audit.md +175 -0
  32. data/docs/research/access-token-revocation-reference.md +81 -0
  33. data/docs/research/alignment-partial-triage.md +50 -0
  34. data/docs/research/claims-reference-contract.md +33 -0
  35. data/docs/research/client-assertion-algorithm-contract.md +177 -0
  36. data/docs/research/completion-error-boundaries.md +56 -0
  37. data/docs/research/consent-api-evolution.md +59 -0
  38. data/docs/research/dpop-reference-contract.md +423 -0
  39. data/docs/research/id-token-hint-reference-contract.md +254 -0
  40. data/docs/research/mtls-reference-contract.md +449 -0
  41. data/docs/research/node-ciba-lifecycle.md +30 -0
  42. data/docs/research/node-oidc-provider-comparison.md +45 -0
  43. data/docs/research/pairwise-reference-contract.md +264 -0
  44. data/docs/research/ping-reference-contract.md +165 -0
  45. data/docs/research/rar-reference-contract.md +55 -0
  46. data/docs/research/refresh-reference-contract.md +85 -0
  47. data/docs/research/registration-reference-contract.md +567 -0
  48. data/docs/research/request-parameter-inventory.md +42 -0
  49. data/docs/research/required-parameter-inventory.md +39 -0
  50. data/docs/research/resource-reference-contract.md +40 -0
  51. data/docs/research/signed-request-reference-contract.md +113 -0
  52. data/docs/resources.md +55 -0
  53. data/docs/security-review.md +294 -0
  54. data/docs/signed-requests.md +162 -0
  55. data/docs/user-code.md +73 -0
  56. data/docs/validation/node-alignment-tests.txt +10 -0
  57. data/examples/client.rb +39 -0
  58. data/examples/demo_app.rb +121 -0
  59. data/examples/dpop_smoke.rb +169 -0
  60. data/examples/ecdh_id_token_smoke.rb +84 -0
  61. data/examples/edwards_id_token_smoke.rb +48 -0
  62. data/examples/edwards_smoke.rb +87 -0
  63. data/examples/edwards_tls_smoke.rb +73 -0
  64. data/examples/encrypted_id_token_smoke.rb +73 -0
  65. data/examples/encryption_tls_smoke.rb +123 -0
  66. data/examples/extensions_smoke.rb +226 -0
  67. data/examples/fixtures/edwards-requests.json +30 -0
  68. data/examples/hmac_id_token_smoke.rb +72 -0
  69. data/examples/migration.rb +18 -0
  70. data/examples/mtls_smoke.rb +395 -0
  71. data/examples/ping_smoke.rb +226 -0
  72. data/examples/registration_smoke.rb +73 -0
  73. data/examples/server.rb +14 -0
  74. data/examples/smoke.rb +46 -0
  75. data/examples/symmetric_id_token_smoke.rb +83 -0
  76. data/lib/rodauth/ciba/authorization_details.rb +60 -0
  77. data/lib/rodauth/ciba/dpop.rb +250 -0
  78. data/lib/rodauth/ciba/http.rb +69 -0
  79. data/lib/rodauth/ciba/id_token_encryption.rb +175 -0
  80. data/lib/rodauth/ciba/jwks_cache.rb +149 -0
  81. data/lib/rodauth/ciba/mtls.rb +174 -0
  82. data/lib/rodauth/ciba/pairwise.rb +105 -0
  83. data/lib/rodauth/ciba/registration.rb +489 -0
  84. data/lib/rodauth/ciba/schema.rb +182 -0
  85. data/lib/rodauth/ciba/version.rb +6 -0
  86. data/lib/rodauth/ciba.rb +4 -0
  87. data/lib/rodauth/features/ciba.rb +2185 -0
  88. metadata +231 -0
@@ -0,0 +1,52 @@
1
+ # CIBA outbound HTTP boundary
2
+
3
+ Signed CIBA request JWKS fetching and protected CIBA-client assertion JWKS
4
+ fetching use a dedicated transport boundary while retaining upstream's
5
+ registered-key lookup and cache. Unrelated OAuth clients keep their existing
6
+ HTTP behavior. Optional ping uses the same boundary with its own
7
+ HTTPS endpoint validation.
8
+
9
+ The default `ciba_http_address_allowed?(address)` rejects special-use address
10
+ ranges, including private/loopback/link-local addresses, multicast and mapped
11
+ IPv4 equivalents. IPAddr parses addresses; text prefixes are not trusted.
12
+ Every DNS answer must pass policy. The connection is pinned to the first
13
+ validated address via Net::HTTP#ipaddr while retaining the original hostname
14
+ for Host and TLS verification. Ambient HTTP proxies are disabled so they cannot
15
+ bypass that destination check. The shared request primitive does not follow
16
+ redirects. Sector-document fetching explicitly follows up to 20 redirects,
17
+ repeating the same destination checks at every hop under one 2.5-second deadline.
18
+ Ping and JWKS fetching retain their existing refusal to follow redirects.
19
+
20
+ For JWKS and sector documents the transport bounds response bodies to 65536 bytes.
21
+ Ping needs only the response status: it closes the connection immediately after
22
+ headers without reading or draining the body. The transport uses a 2.5-second outer
23
+ Ruby timeout, as well as connection/read/write timeouts. Registered JWKS URIs
24
+ retain upstream's HTTP/HTTPS scheme support; production registrations should
25
+ use HTTPS. Userinfo, fragments and other URI schemes are rejected. Transport
26
+ validation failures are invalid_request for signed CIBA authentication requests
27
+ and invalid_client for JWT client authentication. They do not save pending work.
28
+
29
+ If an application deliberately needs an internal endpoint, override
30
+ `ciba_http_address_allowed?` narrowly for that deployment. Returning true allows
31
+ that resolved address. There is no default private-network exception. The
32
+ loopback JWKS fixture overrides only 127.0.0.1 on its test app; this is not a
33
+ production recommendation. Operators must account for the separate risk of
34
+ which clients may register destinations, and retain their egress controls.
35
+
36
+ Compared with node's connected-socket guard, the gem validates all DNS answers
37
+ and pins one before connection. It additionally rejects multicast and
38
+ IPv4-compatible IPv6. It does not implement multi-address connection fallback.
39
+ The allow-policy applies when a network lookup happens; an already cached JWKS
40
+ remains usable until upstream cache invalidation/expiry. Network refusal alone
41
+ is therefore not a key-revocation mechanism.
42
+
43
+ Tests cover IP ranges/mapped forms, default loopback refusal before any receiver
44
+ request, mixed DNS answers, hostname/TLS configuration and IP pinning, real
45
+ loopback cache/rotation/error behavior under a narrow test exception, and an
46
+ oversized response. The DNS pinning test inspects the configured Net::HTTP
47
+ client; it is not an adversarial live DNS-rebinding experiment. Installed-gem
48
+ ping tests directly verify untrusted TLS rejection and a stalled receiver's
49
+ timeout; saved approval survives. Hostname mismatch and expired certificates
50
+ remain unverified.
51
+ The independent node ping harness separately verifies real TLS notifications
52
+ and default loopback rejection; do not transfer that evidence to the gem.
data/docs/pairwise.md ADDED
@@ -0,0 +1,76 @@
1
+ # Optional pairwise CIBA subjects (integration in progress)
2
+
3
+ Pairwise is off by default. First run the explicit migration
4
+ `Rodauth::CibaSupport::Schema.add_pairwise_subject(db)` to add nullable
5
+ `oauth_grants.ciba_subject` (custom table/column supported; configure
6
+ `ciba_token_subject_column` for a renamed column). Enable `ciba_pairwise_enabled true` only together
7
+ with `ciba_pairwise_identifier(account_id, oauth_application:, sector_identifier:)`.
8
+ The callback must return a stable, nonempty ASCII subject of at most 255 bytes.
9
+ The application owns its secret/persistence lifecycle and sector/account mapping;
10
+ the gem does not supply a default hash or a reversible identifier store.
11
+
12
+ This integration supports private_key_jwt and self_signed_tls_client_auth clients
13
+ with registered jwks_uri, and no simultaneous inline jwks. Enable upstream
14
+ `:oauth_jwt_bearer_grant` and create its CIBA replay ledger with
15
+ `Schema.create_client_assertions(db)` if not already migrated. The standard client columns jwks_uri,
16
+ sector_identifier_uri, response_types and redirect_uri must be available where
17
+ used. Existing public clients retain their behavior. Self-signed TLS clients
18
+ require upstream `oauth_tls_client_auth` plus the explicit trusted certificate
19
+ callback described in the [mTLS contract](research/mtls-reference-contract.md).
20
+ The peer certificate must match x5c from the registered jwks_uri; PKI TLS
21
+ authentication alone is not a proof of that key ownership. Signed-request-only
22
+ proof with another client authentication method is rejected, matching the pinned
23
+ node-oidc-provider registration policy even when signed requests are enabled.
24
+ This is a shared policy restriction, not a remaining reference-alignment gap.
25
+
26
+ For CIBA-only clients with no browser response_types, absent sector_identifier_uri
27
+ means the sector is jwks_uri's host including a nondefault port. An explicit
28
+ sector_identifier_uri must be HTTPS. Browser response_types require that explicit
29
+ URI. Its HTTP 200 JSON array must include jwks_uri and, for browser clients, all
30
+ redirect_uris. Fetching uses the bounded address-checked outbound transport;
31
+ successful validation is cached per Rodauth application class using a digest of
32
+ the complete client row, with a 100-entry LRU bound and no time-based expiry.
33
+ Static and dynamically stored clients validate on a cache miss. Registration
34
+ and management replacement always fetch afresh, even for unchanged metadata.
35
+ Failures are not cached; an unsuccessful replacement does not erase the old
36
+ client's valid cache entry. Network I/O occurs outside the cache mutex, so
37
+ concurrent cold lookups may fetch twice. Processes do not share the cache.
38
+ This does not yet implement node's configurable sector-validation bypass policy.
39
+
40
+ Only the emitted subject is pairwise: saved requests, consent and OAuth grants
41
+ retain the canonical account ID. UserInfo must return the same pairwise subject
42
+ as ID Token. With pairwise enabled, new CIBA issuances snapshot their subject in that token
43
+ row. JWT UserInfo checks the signed subject against this snapshot and the bound
44
+ client before using the internal account for lookup. After a management sector
45
+ change, old tokens can therefore authenticate while UserInfo emits the new
46
+ sector subject, as measured with node opaque tokens. Refresh also uses the new
47
+ sector, and already-issued JWT bytes remain unchanged. Older rows with no
48
+ snapshot retain the prior current-subject check; a sector change can invalidate
49
+ those old JWTs. The migration does not invent historical subjects.
50
+
51
+ For id_token_hint, configure the existing `resolve_ciba_id_token_subject` callback
52
+ to recover the canonical account in the authenticated client's context. The gem
53
+ first verifies the ID Token's issuer, audience and signature; the hint then starts
54
+ a new request requiring approval. Do not treat a hashed subject as a new account.
55
+
56
+ Current tests cover opaque/JWT access tokens with verified ID Token, UserInfo,
57
+ app-resolved ID hint, internal identity, same/different sector derivation,
58
+ dynamic registration constraints and stubbed sector document membership. Opaque/JWT refresh also verifies subject and internal-account preservation,
59
+ rotation/replay, wrong-client refusal, and rollback on invalid identifier results.
60
+ Installed-gem tests now fetch sector documents over verified HTTPS and reject
61
+ untrusted certificates, invalid membership/JSON, oversize and HTTP errors without
62
+ persistence or forwarding the registration bearer. Sector fetching now follows 301/302/303/307/308 (up to 20 redirects), validating
63
+ each destination with the outbound policy and limiting the entire chain to
64
+ 2.5 seconds. The measured same-origin HTTPS 302 case agrees with node. Existing-token UserInfo and refresh after a management sector change are now
65
+ verified for opaque/JWT tokens. Pending requests retain their canonical account and unapproved state across a
66
+ sector update; subsequent approved issuance uses the new sector subject. The
67
+ installed-gem TLS fixture now covers hybrid DCR, real remote-key authentication,
68
+ approval, verified ID Token, saved subject and matching UserInfo. Concurrent
69
+ sector management/issuance tests now verify one issuance subject across storage
70
+ and tokens, followed by current-sector UserInfo, on all three DBs. PostgreSQL and
71
+ MySQL force the update to commit while issuance is paused; SQLite tests writer
72
+ contention and either serial order. Concurrent refresh/key changes, arbitrary
73
+ application hooks and additional deployment combinations remain unverified.
74
+ This is not a completed pairwise parity or release-readiness claim.
75
+
76
+ [Reference contract](research/pairwise-reference-contract.md).
data/docs/ping.md ADDED
@@ -0,0 +1,118 @@
1
+ # Optional ping delivery
2
+
3
+ Run the explicit migration and enable the capability:
4
+
5
+ ```ruby
6
+ Rodauth::CibaSupport::Schema.add_ping(db)
7
+ # Rodauth configuration:
8
+ ciba_ping_enabled true
9
+ ```
10
+
11
+ Register each ping client with backchannel_token_delivery_mode = ping and an
12
+ HTTPS backchannel_client_notification_endpoint. Poll clients remain supported.
13
+ Discovery advertises poll/ping only while enabled. The migration adds the client
14
+ endpoint column and a separate ciba_ping_deliveries table, leaving existing
15
+ requests and grants intact. Custom migration keywords: applications_table,
16
+ endpoint_column, table, requests_table, request_key. Configure the corresponding
17
+ Rodauth endpoint-column and ciba_ping_deliveries_table settings when renamed.
18
+
19
+ The initiation request must provide a nonempty client_notification_token using
20
+ Bearer token characters, at most 1024 bytes. Entropy is the client's responsibility.
21
+ Clients must generate at least128 bits of entropy (160 is recommended). The gem
22
+ validates syntax/length, not the randomness of a supplied token. The application
23
+ should verify that the registered notification endpoint is controlled by the
24
+ client; HTTPS validation and destination filtering alone do not prove ownership.
25
+ The receiving client must check the notification bearer and its association
26
+ with auth_req_id before collecting tokens. An OP-side send test does not prove
27
+ that receiver behavior. See [CIBA Core §§7.1,10.2,14](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html).
28
+ Poll ignores this parameter. Signed requests use only its signed inner value.
29
+ Invalid metadata, userinfo/fragments or non-HTTPS endpoints cannot enable a ping
30
+ client. Network destination checks apply at send time using the
31
+ [CIBA HTTP boundary](outbound-http.md), which rejects special-use destinations
32
+ by default. There are no redirects, ambient proxies or automatic retries.
33
+
34
+ ## Persistence and API behavior
35
+
36
+ The private delivery table stores request_id, original auth_req_id, the
37
+ notification token and the registered endpoint at acceptance. The endpoint column
38
+ is retained as acceptance-time data for schema compatibility; dispatch uses the
39
+ current client registration, not that saved endpoint. Unlike the
40
+ request table's hashed identifier, the original identifier is needed to send
41
+ the notification. These fields are recoverable plaintext database values;
42
+ protect access/backups and apply deployment encryption controls as appropriate.
43
+ They are excluded from public request APIs, state-change hook snapshots, device
44
+ snapshots and observation events. The normal request cleanup cascades deletion
45
+ to delivery data. Credentials remain until request cleanup, including after
46
+ successful delivery/collection, to avoid inventing a separate retention system.
47
+
48
+ Approval/denial commits before notification. An outer application transaction
49
+ delays notification until its outermost commit; rollback sends nothing. A
50
+ successful send returns normally from completion. Network/non-200-or-204 errors
51
+ raise PingDeliveryError **after** approval/denial is saved. If completion happens
52
+ inside an outer transaction, that error arises from the outer commit. It does
53
+ not mean the result rolled back. Observers remain independent best-effort hooks;
54
+ notification failures are not suppressed as observation failures.
55
+
56
+ `retry_ciba_ping(request_id)` sends separately from completion and returns
57
+ `:delivered` or `:not_deliverable` for pending, consumed, expired or non-ping
58
+ requests. Unknown IDs raise NotFound. It refuses execution inside a transaction.
59
+ Each send uses the client's current registered HTTPS notification endpoint,
60
+ matching node-oidc-provider's freshly loaded Client behavior. An authenticated
61
+ registration-management update can therefore redirect subsequent notifications,
62
+ including their saved notification credential, to the replacement endpoint.
63
+ A client that no longer supports ping or has invalid metadata raises Ineligible.
64
+ A request already constructed for dispatch keeps its selected endpoint even if
65
+ management updates the registration during delivery; updates do not cancel
66
+ in-flight network operations. Repeated identical completion retains the
67
+ existing idempotent completion behavior; use the retry API to resend.
68
+
69
+ Delivery is a POST with Bearer authentication and a JSON body containing only
70
+ auth_req_id. Approval and denial have the same notification shape. The client
71
+ authenticates separately to obtain tokens or the denial result. Failure to
72
+ deliver does not prevent retrieval of an already saved result.
73
+
74
+ There is no durable automatic retry scheduler or exactly-once claim. An
75
+ application may schedule explicit retries. Concurrent retries can send duplicate
76
+ notifications, and a result can be consumed/expire while a send is in flight.
77
+ The receiver must deduplicate notifications by identifier before collecting
78
+ tokens; fetching twice would trigger replay revocation. Process death after
79
+ commit and before send is another reason to use application-managed retries.
80
+
81
+ ## Evidence and remaining work
82
+
83
+ Integration tests cover approval/denial, exact payload, private snapshots,
84
+ post-commit delivery, rollback, failed-send persistence, retry, expiry, metadata,
85
+ endpoint changes and cascading cleanup. The installed-gem ping smoke uses a
86
+ real HTTPS receiver with a temporary CA and an exact loopback policy exception;
87
+ TLS verification stays enabled. It tests 200/204, 503/302, a stalled response,
88
+ retry and collection. A separate process without trust in the test CA rejects
89
+ TLS before credentials reach the receiver while retaining the saved result.
90
+ The independent [node comparison](research/ping-reference-contract.md) establishes
91
+ the reference save-before-send behavior.
92
+
93
+ Barrier-based tests hold automatic delivery open while another DB connection
94
+ collects tokens, and hold two explicit retries open during collection. There is
95
+ no active DB transaction in the send callback; one token issuance occurs, both
96
+ in-flight notifications may finish, and subsequent retry is not_deliverable.
97
+ A duplicate token collection still revokes the grant. Signed requests with
98
+ user_code use the signed notification token only, ignoring outer replacements.
99
+
100
+ Process-crash recovery, multi-process delivery, hostname-mismatch/expired TLS
101
+ certificates and every feature combination remain unverified. These scenarios
102
+ do not establish full node parity or certification.
103
+
104
+ ### Notification response bodies
105
+
106
+ A200 or204 status acknowledges delivery. The gem closes the response connection
107
+ without reading its body, so an ignored body cannot fail delivery because of its
108
+ size or a body-read timeout. TLS verification, endpoint/address validation,
109
+ redirect rejection and the header-receipt timeout remain enforced. Other HTTP
110
+ consumers (JWKS and sector documents) retain their bounded body reads.
111
+
112
+ [Protocol/transport tests](validation/ping-response-body-current.txt) include a
113
+ response declaring10MB but sending no body: the sender returns after headers
114
+ and the peer observes closure. The [installed TLS fixture](validation/ping-response-body-installed.txt)
115
+ and [reference TLS fixture](validation/node-ping-response-body.txt) both accept
116
+ a200 response containing140,000 ignored bytes. Response-header stalls and failed
117
+ statuses remain separate failures; successful headers do not require a complete
118
+ response body.
@@ -0,0 +1,114 @@
1
+ # Protocol-to-test mapping
2
+
3
+ Push is not implemented by the selected node-oidc-provider 9.12.2 reference or
4
+ this gem. Its conditional requirements are retained as `reference_unsupported`,
5
+ not passing tests. [Measured boundary](research/push-reference-boundary.md).
6
+
7
+ Optional refresh/related revocation coverage is in `test/ciba_refresh_test.rb`,
8
+ `test/ciba_refresh_storage_test.rb`, `test/ciba_refresh_permissions_test.rb`,
9
+ `test/ciba_refresh_hooks_test.rb`, `test/ciba_access_revocation_test.rb` and
10
+ `test/ciba_lock_order_test.rb`. It includes scoped issuance, source persistence,
11
+ rotation/replay, current-consent filtering, transaction hooks, cleanup,
12
+ access-versus-refresh revocation, token provenance and legacy formats. The
13
+ installed extension smoke covers opaque/JWT combinations. See the
14
+ [reference boundaries](research/access-token-revocation-reference.md) before
15
+ interpreting these tests as full OAuth revocation or node behavior equivalence.
16
+
17
+ Implementation extension (not a CIBA Core requirement): optional nonce propagation is tested by `test_nonce_is_bound_to_start_request_not_poll_and_not_observed`, `test_nonce_is_optional_and_bounded`, `test_nonce_forward_migration_preserves_existing_request` and the complete node HTTP harness. These tests do not add a CIBA conformance requirement.
18
+
19
+ Additional optional capabilities have separate coverage: `test/ciba_claims_test.rb` tests explicit claim consent and UserInfo provenance; `test/ciba_resources_test.rb` tests per-resource scope/audience, omission callbacks, saved-consent and original-target binding, JWT/opaque UserInfo rejection, introspection, issuance rollback, concurrent target selection and claims composition. The full node OP harness independently covers comparable claims and resource scenarios. See [claims limits](claims.md) and [resource limits](resources.md); these are not a claim of full OIDC claims or RFC8707 conformance.
20
+
21
+ This is the release's requirement mapping, not a certification claim. Run `ruby test/run.rb`; method names below are in `test/ciba_*_test.rb` unless otherwise noted. The implementation covers the declared subset; omitted optional features are not counted as passing tests.
22
+
23
+ | Reference / concern | Test method(s) |
24
+ |---|---|
25
+ | CIBA §4: Discovery / poll metadata | `test_discovery_describes_poll_and_keeps_existing_oidc_metadata` |
26
+ | CIBA §7, §7.1: TLS, form POST, confidential authentication | `test_non_tls_and_json_requests_are_rejected`, `test_client_authentication_is_required_at_both_endpoints`, `test_post_client_authentication`, `test_basic_credentials_are_form_decoded_and_hashed_secrets_work`, `test_two_authentication_methods_are_rejected` |
27
+ | CIBA §7.1–7.2: hint, scope, unsupported features, unknown parameters | `test_request_validation_and_unknown_parameters` |
28
+ | Optional login_hint_token resolver, exclusive hints, account binding and raw-token exclusion | `test/ciba_hint_token_test.rb`; complete node HTTP comparison and installed-gem extension smoke |
29
+ | Optional id_token_hint signature/issuer/audience/time boundary, retained keys and new approval | `test/ciba_id_token_hint_test.rb`; complete node comparison; installed-gem extension smoke; [validation differences](id-token-hint.md) |
30
+ | Optional signed request metadata, client keys, required claims and inner-only parameters | `test/ciba_signed_request_test.rb`, `test/ciba_remote_jwks_test.rb`; complete node comparison and installed-gem signed requests; [limits](signed-requests.md) |
31
+ | Optional user_code application policy, missing/invalid errors, no raw persistence and new approval | `test/ciba_user_code_test.rb`; complete node comparison and installed-gem signed-request composition; [policy boundary](user-code.md) |
32
+ | Optional request_context application callback, persistence, signed input, failure boundaries and no automatic token claim | `test/ciba_request_context_test.rb`; complete node HTTP harness and installed extension smoke; [contract](request-context.md) |
33
+ | Optional ping: metadata, private delivery data, completion/notification ordering, retry, cleanup | `test/ciba_ping_test.rb`, installed-gem `examples/ping_smoke.rb`, independent node `test/reference/full-op/ping.mjs`; [limits](ping.md) |
34
+ | CIBA §7.1: binding / requested expiry / context | `test_expiry_binding_context_and_offline_access` |
35
+ | OAuth request parsing: repeated parameters / encoding / bounds | `test_duplicate_parameters_are_rejected`, `test_invalid_utf8_and_oversized_body_are_rejected`, `test_rack_inputs_without_rewind_work_on_both_endpoints` |
36
+ | CIBA §7.3: acknowledgement / no-store | `test_expiry_binding_context_and_offline_access`, `test_approved_request_issues_customer_id_token_once` |
37
+ | CIBA §10.1: client-bound request / subject | `test_other_identity_client_and_expiry_cannot_issue` |
38
+ | CIBA §10.1.1 / OIDC Core §2: signed ID Token, issuer/audience/context | `test_approved_request_issues_customer_id_token_once`, `test_id_token_audience_is_client_even_with_different_access_audience`, `test_missing_authentication_context_is_not_fabricated`, `test_unsigned_id_token_client_is_rejected` |
39
+ | CIBA §10.1.1: one successful redemption | `test_approved_request_issues_customer_id_token_once`, `test_concurrent_polls_issue_one_grant` |
40
+ | CIBA §11: pending / slow_down | `test_pending_poll_updates_survive_json_halt`, `test_concurrent_pending_polls_both_receive_retryable_results` |
41
+ | CIBA §11: denial / expiry | `test_denial_and_expired_completion`, `test_other_identity_client_and_expiry_cannot_issue`, `test_expiry_during_before_issue_rolls_back_and_returns_expired` |
42
+ | App completion contract / retry / identity | `test_approval_is_idempotent_and_context_conflicts_are_rejected`, `test_concurrent_approve_and_deny_choose_one_decision`, `test_client_and_account_changes_are_rechecked` |
43
+ | Signing and hook rollback / savepoints | `test_signing_failure_rolls_back_consumption_and_grant_then_allows_retry`, `test_hooks_see_before_and_after_states_and_failure_rolls_back`, `test_completion_hook_failure_rolls_back_and_retry_does_not_repeat_hooks`, `test_failure_reported_inside_outer_transaction_still_rolls_back_inner_work` |
44
+ | Commit observations / rollback / callback failure | `test_observation_waits_for_outer_commit_and_does_not_change_result`, `test_outer_rollback_discards_issuance_and_observation`, `test_savepoint_rollback_discards_event_even_when_outer_transaction_commits`, `test_request_callbacks_after_commit_and_observer_secrets`, `test_acceptance_hook_rollback_does_not_notify` |
45
+ | Reentrancy / immutable lookup / recovery / cleanup | `test_reentrant_completion_is_rejected_even_from_another_auth_instance`, `test_request_lookup_recovery_and_cleanup`, `test_expired_pending_cleanup_and_live_approved_retention` |
46
+ | Schema / migration / uniqueness | `test_custom_table_and_column_mapping`, `test_migration_client_metadata_helper`, `test_explicit_migration_up_and_down`, `test_schema_rejects_duplicate_public_identifier` |
47
+ | Upstream coexistence / initial restrictions | `test_authorization_code_flow_still_uses_oidc_wrapper`, `test_dcr_rejects_ciba_but_preserves_other_grant_validation`, `test_unavailable_or_unsupported_client_metadata_is_rejected`, `test_jwt_access_token_configuration_also_works` |
48
+ | Demo's browser transport | `DemoTest#test_browser_approval_checks_csrf_and_finishes_request`, `test_default_require_loads_jwt_backend_in_fresh_process` |
49
+
50
+ ## Primary specifications
51
+
52
+ - [CIBA Core 1.0](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)
53
+ - [OIDC Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
54
+ - [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749.html)
55
+
56
+ Product policies (size limits, offline_access removal, static/public restriction, internal API semantics) are distinguished in [the API documentation](api.md). The test suite verifies those policies; it does not imply they are universally required by the specifications.
57
+
58
+ ## Conformance Suite cases consulted
59
+
60
+ The following files were read from the OpenID Foundation's read-only mirror at commit `fc6a65c4ac25a7f1fc8a92205ebc5836f321189d` on2026-09-29:
61
+
62
+ - [FAPICIBAID1AuthReqIdExpired](https://github.com/openid-certification/conformance-suite/blob/fc6a65c4ac25a7f1fc8a92205ebc5836f321189d/src/main/java/net/openid/conformance/fapiciba/FAPICIBAID1AuthReqIdExpired.java): requested lifetime followed by an expired collection attempt. The local tests use a controlled clock/DB deadline instead of sleeping.
63
+ - [FAPICIBAID1EnsureAuthorizationRequestWithMultipleHintsFails](https://github.com/openid-certification/conformance-suite/blob/fc6a65c4ac25a7f1fc8a92205ebc5836f321189d/src/main/java/net/openid/conformance/fapiciba/FAPICIBAID1EnsureAuthorizationRequestWithMultipleHintsFails.java): multiple hints are rejected. Local coverage exercises login_hint combined with the other hint parameters.
64
+ - [FAPICIBAID1EnsureAuthorizationRequestWithBindingMessageSucceeds](https://github.com/openid-certification/conformance-suite/blob/fc6a65c4ac25a7f1fc8a92205ebc5836f321189d/src/main/java/net/openid/conformance/fapiciba/FAPICIBAID1EnsureAuthorizationRequestWithBindingMessageSucceeds.java): ordinary binding-message success. The local suite validates storage and the demo displays the message with HTML escaping.
65
+
66
+ These are references for overlapping behaviors only. Their FAPI profile setup is not implemented here. No Suite code is copied into the gem, and neither the complete Suite nor certification tests have been run.
67
+
68
+ ## Client authentication compatibility follow-up
69
+
70
+ CIBA Core §4 shares the OP's `token_endpoint_auth_methods_supported` metadata; it does not require basic/post-only OP configuration. §7.1 requires registered client authentication and accepts issuer/token/backchannel audiences for JWT assertions at the Backchannel Endpoint.
71
+
72
+ - `test_upstream_jwt_authentication_coexists_with_ciba_and_discovery`: private_key_jwt CIBA start/poll, all three start audiences, existing authorization_code, Basic CIBA on the same OP, unchanged Discovery support.
73
+ - `test_ciba_jwt_authentication_rejects_wrong_audience_signature_and_mixed_credentials`: wrong audience/signature and duplicate authentication rejected.
74
+ - `test_upstream_client_secret_jwt_works_for_ciba`: upstream shared-secret JWT start/poll.
75
+ - `test_public_clients_can_exist_but_cannot_use_ciba`: public-client configuration is allowed for the OP, excluded from CIBA.
76
+
77
+ These tests do not claim all upstream authentication mechanisms are validated. JWT client authentication is distinct from the opt-in signed CIBA authentication requests covered above. See the following additional integration boundaries.
78
+
79
+ ## Additional authentication and registration coverage
80
+
81
+ | Boundary | Current evidence |
82
+ |---|---|
83
+ | JWT assertion issuer audience at refresh, wrong audience and replay | `test/ciba_assertion_refresh_test.rb`, `test/reference/full-op/assertion-refresh.mjs` |
84
+ | Discovery retention of configured introspection, DPoP algorithms and mTLS capability | `test/ciba_discovery_extensions_test.rb`; installed smoke follows discovered URLs |
85
+ | DPoP proof, nonce policy, replay, UserInfo, introspection and refresh | `test/ciba_dpop_test.rb`, `test/ciba_dpop_registration_test.rb`, `examples/dpop_smoke.rb`, `test/reference/full-op/dpop.mjs`; [contract](research/dpop-reference-contract.md) |
86
+ | mTLS trust callbacks, exact certificate binding, opaque/JWT UserInfo and introspection, refresh | `test/ciba_mtls_test.rb`, `examples/mtls_smoke.rb`, `test/reference/full-op/mtls.mjs`; [contract](research/mtls-reference-contract.md) |
87
+ | mTLS remote JWKS rotation, retrieval failure and recovery | `test/ciba_mtls_remote_test.rb`; installed `examples/mtls_smoke.rb` uses verified HTTPS and forced cache expiry |
88
+ | mTLS registration policy and management replacement | `test/ciba_mtls_registration_test.rb`; node and installed TLS fixtures |
89
+ | Dynamic registration/management, metadata, credentials and concurrency | `test/ciba_registration_test.rb`, `test/ciba_registration_remote_test.rb`, `examples/registration_smoke.rb`; [contract](research/registration-reference-contract.md) |
90
+ | Pairwise subjects, hints, refresh, sector documents and management | `test/ciba_pairwise_test.rb`, installed `examples/ping_smoke.rb`; [contract](research/pairwise-reference-contract.md) |
91
+
92
+ These rows map integration boundaries, not every normative requirement in the
93
+ referenced RFCs. Application-owned endpoint aliases have distinct-port TLS
94
+ coverage; arbitrary proxy/path rewriting and distributed cache behavior do not.
95
+ Consult [release validation](release-validation.md) for the tested Ruby/DB
96
+ combinations; file presence alone is not proof of a successful run.
97
+
98
+ ## Manual requirements and security review
99
+
100
+ The historical 2026-09-29, 159-item initial-scope classification is in [requirements-audit.md](requirements-audit.md). Its excluded optional features do not describe the subsequently implemented extensions. An incremental [current audit overlay](requirements-audit-current.md) now records reviewed and pending IDs; the full normative re-audit remains incomplete; [the alignment roadmap](alignment-roadmap.md) tracks current evidence and gaps. Classification separates mandatory, conditional, recommended and optional behavior and identifies gem/upstream/application/client responsibilities. The Jev scores are historical inputs, not compliance evidence.
101
+
102
+ Additional direct tests in `test/ciba_security_test.rb` cover OIDC Core §9 required assertion claims, issuer/subject/client binding, malformed assertions, missing client keys, short HMAC keys, replay across endpoints/flows, concurrent replay, bounded ledger cleanup and persistent slow_down growth. `test/ciba_requirements_test.rb` covers acknowledgement identifier format, exact expiry boundary, HTTP401 authentication errors, missing-vs-invalid scope and optional error-description ASCII constraints. See [security-review.md](security-review.md) for findings and test limits.
103
+
104
+ ## node-oidc-provider alignment
105
+
106
+ `test/ciba_alignment_test.rb` verifies the v9.12.2 binding-message default and application replacement policy. The demo XSS/CSRF test explicitly relaxes that policy to continue checking output escaping. See [alignment progress](node-alignment.md); this is not a claim that the remaining API or protocol differences are resolved.
107
+
108
+ `test/ciba_grant_test.rb` verifies persisted consent, client/account binding, requested/approved scope intersection, untrusted snapshot-field rejection, consent expiry/revocation at collection, issuance/revocation races, revocation after request cleanup, completion retries and transactional rollback. That file alone does not cover node-specific claim/resource/RAR consent. Separate tests cover those extensions; executed PostgreSQL/MySQL evidence is recorded in [release validation](release-validation.md).
109
+
110
+ - AD initiation integration: `test/ciba_alignment_test.rb` verifies saved request handoff before response, synchronous completion, missing implementation/dispatch failure, and rejection before dispatch. This application callback is an implementation boundary, not a standardized CIBA approval HTTP API.
111
+
112
+ - Shared OIDC issuance: `test_common_oidc_claim_provider_only_receives_approved_scopes` verifies account claims come from the upstream provider using effective consent scopes, without browser-session authentication-time lookup. Full claims-parameter consent and resource/RAR support remain outside this evidence.
113
+
114
+ - Completion consistency: `test_grant_expiry_during_before_approve_does_not_record_approval` and `test_grant_revocation_during_before_approve_rolls_back_the_completion` check revalidation after transactional hooks. Demo tests cover saved consent reuse, conflicting decision rollback, and another customer’s request rejection.
@@ -0,0 +1,151 @@
1
+ # CIBA refresh tokens — implementation in progress
2
+
3
+ Disabled by default. This branch implements the basic flow, but it is not yet a
4
+ release-ready refresh implementation. The implemented revocation boundaries and
5
+ legacy-format differences below still need the overall alignment review. Do not
6
+ infer full reference parity from the existence of the flag.
7
+
8
+ For development tests, explicitly create storage with
9
+ `Rodauth::CibaSupport::Schema.create_refresh_tokens(db)` and configure
10
+ `ciba_refresh_tokens_enabled true`. Register both the CIBA grant and
11
+ `refresh_token` for the client, and allow `offline_access` in both OP and client
12
+ scopes. Initial issuance requires `offline_access` in both the CIBA source and
13
+ its saved consent, as well as the
14
+ refresh grant registration, matching the reference default policy.
15
+
16
+ `ciba_refresh_tokens_table` defaults to `:ciba_refresh_tokens` and
17
+ `ciba_refresh_token_lifetime` to 14 days. Only the digest of an opaque, namespaced
18
+ refresh token is stored. The source retains original scope/resources/claims/RAR,
19
+ account/client, consent ID and authentication context separately from each access
20
+ token. Short-lived CIBA request cleanup does not remove it.
21
+
22
+ Updates validate the authenticated client, current grant registration, account,
23
+ saved consent and enabled optional capabilities. Removing `offline_access` from
24
+ the saved consent rejects subsequent refreshes with `invalid_grant` before
25
+ rotation or issuance. Scope narrowing applies to the
26
+ current access token, not the saved refresh source. Resource selection is computed
27
+ against the current consent. Each update creates a new access-token record.
28
+
29
+ When upstream JWT client authentication is enabled, `private_key_jwt` and
30
+ `client_secret_jwt` accept the OP issuer as the assertion audience for CIBA
31
+ refresh, as they do for initial CIBA token collection. The current token URL
32
+ remains accepted. This extension applies only to the CIBA grant and reserved
33
+ `ciba_rt_` refresh namespace; ordinary OAuth token requests retain their existing
34
+ audience policy. Wrong audiences and assertion replay remain authentication
35
+ failures. [Measured comparison](research/refresh-reference-contract.md#jwt-client-assertion-audience).
36
+
37
+ For confidential clients the initial implementation follows node's default
38
+ rotation policy: rotate after 70% of token lifetime, until one year after initial
39
+ issuance. A fresh token can be reused. Rotation preserves consumed digests; reuse
40
+ by the correct client revokes the saved consent and its linked access tokens, and
41
+ therefore also invalidates successor refresh tokens. A different client's request
42
+ does not revoke the rightful client's consent. Simultaneous rotation can produce
43
+ one success followed by a replay response that invalidates the returned successor.
44
+
45
+ Token consumption, rotation and issuance share the DB transaction. Signing failure
46
+ rolls them all back, preserving the project's accepted atomic-issuance contract.
47
+ This differs from node's nontransactional memory adapter. Existing non-CIBA OAuth
48
+ refresh tokens continue through upstream's implementation and rotation policy.
49
+
50
+ The flag can be disabled without querying the optional table for presented CIBA
51
+ refresh tokens; their namespace causes `invalid_grant`. This namespace is reserved
52
+ for the extension. Changing ordinary OAuth token generators to use `ciba_rt_`
53
+ would conflict with that dispatch boundary.
54
+
55
+ See the [reference contract](research/refresh-reference-contract.md) for measured
56
+ reference behavior and the remaining tests. Current authentication context output
57
+ follows the gem's CIBA ID Token behavior; it is not identical to node's optional
58
+ claim masking.
59
+
60
+ ## Refresh-token revocation
61
+
62
+ Enable upstream `:oauth_token_revocation` to expose its advertised endpoint.
63
+ The CIBA extension also adds its URL to OIDC Discovery when that feature is enabled.
64
+ Presenting a `ciba_rt_` token authenticates the OAuth client (never the browser
65
+ account), requires the CIBA form/TLS transport and revokes the entire saved
66
+ consent and linked access tokens. This remains possible with refresh issuance
67
+ disabled when the refresh table exists. The standard upstream before/after revoke
68
+ hooks run in the transaction; an exception rolls the revocation back.
69
+
70
+ Like the measured reference, missing, wrong or unknown `token_type_hint` does not
71
+ prevent finding a CIBA refresh token. Success is HTTP 200 with an empty body.
72
+ Unknown, expired and already-revoked tokens are successful no-ops. An authenticated
73
+ different client presenting a live token receives `invalid_request`; it cannot
74
+ revoke the owner's consent. After revocation, its retry is also a successful no-op.
75
+
76
+ The endpoint also recognizes saved opaque CIBA access tokens independently of the
77
+ hint. Their revocation invalidates related tokens and deletes old request/refresh
78
+ sources while retaining saved consent, matching the [reference's distinct AT
79
+ policy](research/access-token-revocation-reference.md). Standard transactional
80
+ revoke hooks still apply. New opaque CIBA tokens use the reserved `ciba_at_`
81
+ namespace, so unknown or cleaned-up tokens return empty 200 after client
82
+ authentication. Legacy unprefixed tokens need a saved row to be recognized; after
83
+ row cleanup they retain upstream behavior. Calling the consent-revocation API
84
+ instead would not preserve consent.
85
+
86
+ New CIBA JWT access tokens always carry the signed private issuance ID, even with
87
+ explicit claims disabled. It is removed from ID Tokens. At revocation the marker
88
+ selects a rejecting path, then OP signature/issuer/type are verified. Proven CIBA
89
+ JWTs return `unsupported_token_type`, including expired ones; forged markers
90
+ return `invalid_request`. This provenance-only verifier never authorizes token use
91
+ and does not alter the normal access-token time checks. Node instead rejects all
92
+ structured JWTs by header shape; unrelated and unmarked legacy JWTs keep upstream
93
+ behavior here. No JWT revocation path mutates saved consent.
94
+
95
+ The issuance marker also binds new JWT UserInfo access to its saved token row.
96
+ Deleting that row invalidates UserInfo access. Unmarked historical JWTs retain the
97
+ old scope-based path without borrowing explicit claims from another issuance.
98
+
99
+ ## Claims, authorization details and cleanup
100
+
101
+ Opaque and JWT access-token tests cover claims/RAR with rotation. The original
102
+ request fields are preserved in the successor source; the application's RAR
103
+ policy is re-evaluated against saved consent for each update. A token-time claims
104
+ parameter does not grant additional attributes. Current rejected claims suppress
105
+ attribute reads and output, and disabled capabilities reject updates instead of
106
+ silently dropping restrictions. Invalid RAR narrowing rolls rotation back.
107
+
108
+ The reference test application's policy emits `source.rar`, explicitly supplied
109
+ at completion; it ignores token-time expansion. The gem's test policy intersects
110
+ the saved request and current consent, with optional narrowing. These are different
111
+ application-owned policies, not proof of identical arbitrary RAR semantics.
112
+
113
+ Run `cleanup_ciba_refresh_tokens(limit: 100)` from an application-owned maintenance
114
+ job after creating the refresh table. It deletes at most the limit of sources whose
115
+ expiry is at or before the current time, and returns the deleted count. Unexpired
116
+ consumed sources must remain for replay detection. Cleanup does not delete saved
117
+ consent or access tokens. It participates in an outer DB transaction and does not
118
+ schedule itself.
119
+
120
+ ## Update hooks and events
121
+
122
+ `before_ciba_refresh` and `after_ciba_refresh` run inside the token transaction.
123
+ They can use `ciba_current_refresh`, an immutable snapshot of the presented refresh
124
+ source with its digest removed. This is not a CIBA request snapshot: its `id` is
125
+ the refresh-source row ID. The after snapshot reflects consumption of that source
126
+ when rotation occurred. Neither snapshot includes a raw token. It is cleared on
127
+ exit, and same-source reentrant updates are rejected.
128
+
129
+ The source, account, client registration, saved consent, expiry and enabled
130
+ capabilities are revalidated after the before hook. Hook exceptions roll back
131
+ source consumption, successor creation and access-token issuance. As with other
132
+ transactional hooks, external side effects cannot be rolled back by the gem.
133
+
134
+ A successful update schedules an immutable `token_refreshed` observation event
135
+ after the outermost commit. It has `version`, `event_id`, `type`, `occurred_at`,
136
+ `refresh_token_id`, `grant_id`, `account_id`, `oauth_application_id` and `rotated`.
137
+ It has no `request_id` because refresh can outlive request cleanup. Tokens, token
138
+ digests, claims and authorization details are not included. Observer exceptions
139
+ go through `report_ciba_observer_error` and do not change the committed result.
140
+ This event describes a committed issuance, not proof of delivery to the client.
141
+
142
+ ## Installed artifact verification
143
+
144
+ `bin/verify-package` builds and installs the gem in a temporary directory, clears
145
+ checkout load-path variables and verifies that the feature is loaded from that
146
+ installation. The extension smoke runs both opaque and JWT modes with signed
147
+ authentication requests, claims/resources/RAR and refresh enabled. It verifies
148
+ original identity context, rotation, grant replay revocation invalidating a
149
+ successor, Discovery-based refresh revocation and expiry cleanup. Existing basic
150
+ and real HTTPS ping smoke also run. Dependencies are reused from local installed
151
+ gems; fresh network dependency resolution and publication are not tested.
@@ -0,0 +1,75 @@
1
+ # 0.1.0 release readiness
2
+
3
+ RubyGems publication is pending. This document separates the supported release
4
+ scope from historical development notes and opt-in experiments.
5
+
6
+ ## Scope
7
+
8
+ The default integration uses poll, login_hint, public subjects and static
9
+ confidential client registration. The README and individual feature contracts
10
+ specify optional capabilities and application responsibilities. Refresh tokens,
11
+ pairwise subjects and dynamic CIBA registration remain experimental, disabled by
12
+ default and excluded from the initial supported release scope. Push and encrypted
13
+ ID Token hints are not implemented. This is not OpenID Certified.
14
+
15
+ The gem requires Ruby >=3.3 and the dependency versions in its gemspec. Application
16
+ DB adapters, account/OAuth schemas, trusted identity resolution, approval UI,
17
+ notification transport and business authorization are application responsibilities.
18
+
19
+ ## Published baseline evidence
20
+
21
+ The [authentication-fix commit](https://github.com/kajisha/rodauth-ciba/commit/3791a53caae7ab6fbe6793fdb1646d7e5074c951)
22
+ passed [hosted CI](https://github.com/kajisha/rodauth-ciba/actions/runs/36710533803):
23
+ Ruby 3.3/3.4/4.0 SQLite and the Docker SQLite/PostgreSQL/MySQL job.
24
+ Its local full run passed410 tests /10,844 assertions with two existing SQLite
25
+ row-lock skips. The latest pre-release cleanup verification is recorded below.
26
+
27
+ Jev's3.95/4 score (confidence0.96) concerned only the missing-client-authentication
28
+ fix. It is not an overall conformance score or a release gate.
29
+
30
+ ## Cleanup review
31
+
32
+ - README, CHANGELOG and the release goal now distinguish the current release
33
+ scope from intermediate implementation restrictions. Original changelog notes
34
+ are preserved in the repository's pre-release development history.
35
+ - The gemspec declares homepage, source, issue tracker and changelog URLs.
36
+ - Static method, constant and instance-variable reference searches were checked
37
+ against Rodauth's dynamic hooks, upstream overrides and public migration APIs.
38
+ A single textual occurrence alone is not evidence of dead code.
39
+ - Removed an unreachable recipient-nil check and safe navigation in ID Token
40
+ encryption. The candidate array is local and checked for emptiness before
41
+ min_by, so successful selection always returns a pair.
42
+ - No broad authentication/state-machine refactor was justified for this release.
43
+ Existing security boundaries and separate extension modules are retained.
44
+
45
+ Test execution information is used to identify review candidates, not to prove
46
+ that every unused branch is dead or every feature combination is covered.
47
+
48
+ ## Current cleanup verification
49
+
50
+ The full suite before the one-line recipient-selection simplification passed410
51
+ tests /10,844 assertions, zero failures/errors, with two existing SQLite row-lock
52
+ skips. Runtime line coverage recorded2369 of2449 executable library lines;
53
+ 13 uncalled method definitions were application-overridden callbacks or default
54
+ error reporters, not unused APIs. All other recorded library method definitions
55
+ were exercised. This is execution evidence from Ruby4.0.6/SQLite, not branch
56
+ coverage or proof of absence of dead code.
57
+
58
+ [Full test log](https://github.com/kajisha/rodauth-ciba/blob/main/docs/validation/release-code-review-tests.txt)
59
+ and [per-file execution summary](https://github.com/kajisha/rodauth-ciba/blob/main/docs/validation/release-code-coverage.json)
60
+ are kept in the repository. Line locations refer to the pre-cleanup source.
61
+
62
+ After removing the redundant check, the encryption regression selection passed
63
+ 76 tests /4,873 assertions, zero failures/errors, with one existing SQLite skip
64
+ ([log](https://github.com/kajisha/rodauth-ciba/blob/main/docs/validation/release-encryption-cleanup-tests.txt)).
65
+ The selection includes the shared integration tests loaded by these test files.
66
+
67
+ The updated gem passed `bin/verify-package`: build, temporary installation outside
68
+ the checkout, and all installed HTTP/TLS smoke flows
69
+ ([log](https://github.com/kajisha/rodauth-ciba/blob/main/docs/validation/release-cleanup-package.txt)).
70
+ Dependencies were reused from local installations; this does not test fresh
71
+ network dependency resolution. After recording these results, the artifact was
72
+ rebuilt with identical runtime code and updated documentation; its final SHA256
73
+ is in `pkg/rodauth-ciba-0.1.0.gem.sha256` (local build output, not committed).
74
+ The cleanup has not rerun the full Ruby/DB matrix locally; the hosted results
75
+ above are for the preceding commit.