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,288 @@
1
+ # CIBA ID Token encryption
2
+
3
+ > Latest policy supersedes the historical minimum-floor notes below: use explicit
4
+ > HTTP freshness, and permit failed-fetch reuse only with publisher stale-if-error,
5
+ > capped at60 seconds without renewing deadlines. See [operations](operations.md#remote-jwks-cache-lifetime).
6
+
7
+
8
+ > Cache policy update: the user approved a minimum60-second successful JWKS
9
+ > lifetime shared by CIBA authentication and encryption, without a new policy
10
+ > switch. Earlier shorter-TTL comparisons below describe the previous runtime.
11
+ > Failed-refresh reuse remains excluded; expiry followed by failed retrieval
12
+ > does not renew freshness. See the current alignment closeout and Jev review.
13
+
14
+
15
+ Enable recipient encryption explicitly:
16
+
17
+ ```ruby
18
+ ciba_id_token_encryption_enabled true
19
+ ```
20
+
21
+ For RSA encryption, register the client's public `jwks` or HTTPS `jwks_uri`. Select
22
+ `id_token_encrypted_response_alg` plus `id_token_encrypted_response_enc`.
23
+ Create storage columns for those two strings and the selected key metadata.
24
+ Dynamic registration defaults omitted `enc` to `A128CBC-HS256` when `alg` is
25
+ present; `enc` without `alg` is rejected. Discovery publishes enabled ID Token
26
+ encryption algorithms without widening UserInfo/request-object encryption lists.
27
+ The default flag is false. Configured encryption must never fall back to a signed
28
+ plaintext ID Token; a failure rolls back issuance and leaves approval available.
29
+
30
+ With the capability disabled, dynamic registration treats the two encryption
31
+ fields as unsupported metadata: they are ignored, omitted from its response,
32
+ and not stored. This follows the measured reference behavior. Clients must
33
+ inspect returned registration metadata. This does not erase a statically
34
+ configured encryption requirement or silently bypass one during issuance.
35
+
36
+ The implementation supports RSA-OAEP and RSA-OAEP-256 recipient keys, plus
37
+ client-secret-derived `dir`, A128KW/A192KW/A256KW and
38
+ A128GCMKW/A192GCMKW/A256GCMKW, and ECDH-ES with optional AES-KW. These use A128GCM/A192GCM/A256GCM and A128CBC-HS256/A192CBC-HS384/A256CBC-HS512 content
39
+ encryption. It first signs the ID Token, then encrypts it using `jwe`1.1.1
40
+ and the CIBA-local GCMKW/ECDH adapters.
41
+ RSA and Edwards ID Token signing both use this path, including refresh.
42
+ The outer protected header carries cty=JWT, selected kid (if present), issuer
43
+ and audience; no compression is enabled.
44
+
45
+ For RSA algorithms, only registered client RSA keys of at least 2048 bits qualify. Optional alg must
46
+ match; optional use must be enc. If key_ops is declared, selection requires
47
+ encrypt and wrapping requires wrapKey. Both operations must be present to issue.
48
+ A missing metadata-compatible recipient returns400 invalid_client_metadata;
49
+ selected unusable key material or operation declarations fail processing. None
50
+ of these failures falls back to an OP key. Existing JWKS retrieval controls
51
+ apply. Do not supply a client's private key to the OP.
52
+
53
+ When several keys qualify, keys explicitly matching alg and use have higher
54
+ priority than keys omitting those fields. Equal scores preserve their input
55
+ order, matching node-oidc-provider. Actual reference issuance and the gem's
56
+ regression put a generic key first and verify that the explicitly tagged second
57
+ key receives the encrypted token. [Before correction](validation/id-token-recipient-priority-red.txt),
58
+ [three-DB regression](validation/id-token-recipient-priority-matrix.txt),
59
+ [reference](validation/node-encrypted-id-tokens.txt).
60
+
61
+ Encrypted ID Token *hints* remain unsupported, matching the measured reference
62
+ behavior; clients can decrypt an issued ID Token before using its signed inner
63
+ token as a hint. This is distinct from encrypted output support.
64
+
65
+ Evidence: [Ruby regression](validation/id-token-recipient-encryption-current.txt),
66
+ [three databases](validation/id-token-recipient-encryption-matrix.txt),
67
+ [Node jose independent decryption](validation/id-token-recipient-encryption-jose.txt),
68
+ and [reference OP](validation/node-encrypted-id-tokens.txt).
69
+ The cross-library probe covers 36 algorithm combinations, initial issuance and
70
+ refresh, inner signature verification and ciphertext tampering. Its temporary
71
+ export includes an ephemeral synthetic recipient private key and must be removed
72
+ after checking; it is not a committed fixture. Run the Ruby test
73
+ `test_ciba_id_token_recipient_encryption` with `CIBA_ENCRYPTED_ID_TOKEN_EXPORT`
74
+ set to a temporary path, then run `verify-gem-encrypted-id-tokens.mjs` with that
75
+ path in the reference Node environment.
76
+
77
+ This is an initial subset of node-oidc-provider encryption capabilities. ECDH,
78
+ symmetric recipient encryption and all metadata-management/remote-key
79
+ combinations remain outstanding.
80
+
81
+ `examples/encrypted_id_token_smoke.rb` now runs from `bin/verify-package` after
82
+ installing the built gem into a temporary directory. It exercises RS256,
83
+ Ed25519 and EdDSA signatures with RSA-OAEP-256/A256GCM, opaque/JWT access tokens,
84
+ initial issuance and refresh, recipient decryption and inner signature checks.
85
+ It uses Rack requests and inline recipient keys; this does not establish remote
86
+ HTTPS recipient retrieval. [Package evidence](validation/id-token-encryption-installed.txt).
87
+ [Ruby 3.3/3.4 regression](validation/id-token-encryption-rubies.txt) covers the
88
+ full suite on SQLite; two row-lock tests remain skipped on that database.
89
+
90
+ ## HTTPS recipient retrieval and failure atomicity
91
+
92
+ The initial encryption path unintentionally used upstream's unguarded HTTP
93
+ lookup during issuance. The [installed regression before correction](validation/id-token-encryption-https-red.txt)
94
+ shows a forbidden loopback recipient being contacted and a token issued.
95
+ The HTTP wrapper now covers CIBA issuance/refresh recipient retrieval as well
96
+ as assertion/request keys. Upstream HTTP authorization failures during issuance
97
+ raise an exception rather than halting with a response inside the transaction;
98
+ this preserves source rollback.
99
+
100
+ The [installed HTTPS probe](validation/id-token-encryption-https-installed.txt)
101
+ runs with an explicitly trusted local certificate and a separate untrusted
102
+ process. It verifies default loopback denial, explicit fixture-only allowance,
103
+ TLS certificate rejection before the handler, GET without forwarded credentials,
104
+ old-key cache retention, new-key use after explicit cache eviction, 503/malformed
105
+ JSON failure rollback and subsequent recovery/refresh rotation in opaque and
106
+ JWT access-token modes. Recipient decryption and the inner RSA signature are
107
+ checked throughout. Real TTL passage is not measured; eviction is explicit.
108
+ The private key never leaves the test recipient.
109
+
110
+ [Database regressions](validation/id-token-encryption-remote-matrix.txt) also
111
+ exercise redirects and oversized bodies during initial issuance and refresh.
112
+ These use HTTP fixtures with an explicit loopback allowance; TLS evidence comes
113
+ from the installed probe. The earlier supported-Ruby matrix predates this HTTP
114
+ correction. Broader signing/encryption combinations and deployment conditions
115
+ remain outstanding; remote-cache and management evidence follows below.
116
+
117
+ After the HTTP correction, [full Ruby 4.0.6 regression](validation/id-token-encryption-https-full.txt)
118
+ passes 348 tests /5,576 assertions on SQLite with two row-lock skips; the
119
+ targeted three-DB matrix passes seven tests /802 assertions per database.
120
+
121
+ ## Reference remote cache and management comparison
122
+
123
+ `test/reference/full-op/encryption-remote.mjs` exercises actual reference HTTP
124
+ endpoints and key retrieval with a narrowly allowed local fixture URL. Live
125
+ cache retains the old recipient; explicit expiry retrieves the replacement.
126
+ Recipient decryption and inner signature/issuer/audience/subject are verified.
127
+ No Authorization header reaches the key server.
128
+ [Reference execution](validation/node-encryption-remote.txt).
129
+
130
+ One concrete difference remains after failed recipient-key refresh. With 503
131
+ and valid JSON, the reference first returns 400 invalid_client_metadata, then
132
+ uses retained keys for the next token refresh without refetching while the key
133
+ endpoint still fails. The gem rolls back and returns 500 server_error; fetch
134
+ failures are not inserted as successful cache entries. Invalid JSON causes
135
+ repeated refetch/failure in both implementations, with those different error
136
+ codes; repairing the response restores issuance.
137
+
138
+ The reference `ClientKeyStore.refresh` updates freshness before checking HTTP
139
+ status, but parses JSON before updating freshness. This explains the observed
140
+ difference. It also imposes at least 60 seconds of freshness; the gem's upstream
141
+ HTTP cache honors no-cache without this floor. These probes force expiry rather
142
+ than waiting for real TTL. Lifetime and post-failure key retention are unresolved
143
+ alignment differences, not claimed equivalence or an approved new adaptation.
144
+ No cache behavior was changed by this comparison.
145
+
146
+ The reference also dynamically registers an encrypted client and updates its
147
+ inline recipient via the management endpoint. Invalid encryption metadata leaves
148
+ the old key intact; successful replacement changes the recipient for existing
149
+ refresh credentials while the original token still decrypts with the old key.
150
+ Gem tests `test_ciba_encryption_management_replaces_recipient_opaque` and
151
+ `test_ciba_encryption_management_replaces_recipient_jwt` verify those boundaries
152
+ in both access-token modes. [Database execution](validation/id-token-encryption-management-matrix.txt).
153
+ This does not prove every management transition, remote URI change or concurrent
154
+ registration update.
155
+
156
+ ### Defaults, removal and disabled registration
157
+
158
+ The management probes now also omit enc while retaining alg: both implementations
159
+ replace the previous A256GCM selection with A128CBC-HS256 and encrypt subsequent
160
+ refresh responses accordingly. Removing both encryption fields clears the
161
+ stored requirement and returns signed ID Tokens on existing refresh credentials.
162
+ The tests follow rotated registration access tokens after each successful PUT;
163
+ reusing the previous management token correctly fails authentication.
164
+
165
+ Disabled-capability registration originally stored and returned the encryption
166
+ fields, although issuance could not honor them. An initial proposal to reject
167
+ registration was revised after the reference returned 201 while omitting both
168
+ fields. The implementation now ignores them, matching the reference's response
169
+ and stored client. [Reference registration](validation/node-encryption-disabled-registration.txt),
170
+ [reference updates](validation/node-encryption-remote.txt),
171
+ [final DB matrix](validation/id-token-encryption-config-matrix.txt).
172
+ This correction is confined to CIBA registration; ordinary OAuth clients follow
173
+ their existing upstream handling.
174
+
175
+ ## Client-secret-derived encryption
176
+
177
+ `dir`, AES-KW and AES-GCMKW do not require recipient JWKS. The OP derives
178
+ bytes from the UTF-8 client secret using SHA-256 for lengths up to32 bytes,
179
+ SHA-384 up to48, and SHA-512 up to64, truncating to the required length. AES-KW
180
+ uses the wrapping-key size; `dir` uses the content-encryption key size (the
181
+ full combined MAC/encryption size for CBC-HS). This follows the pinned
182
+ node-oidc-provider9.12.2 client model and its actual issuance/refresh output.
183
+ It is distinct from HMAC signing, which uses the original secret directly.
184
+
185
+ `ciba_id_token_encryption_secret(oauth_application:)` retrieves the original
186
+ secret and by default delegates to `ciba_id_token_hmac_secret`. Verified
187
+ Basic/POST credentials can be reused within that authenticated request even
188
+ with hash storage. Explicit plaintext storage is the fallback. Applications
189
+ using private-key authentication with hashed secrets must supply an original
190
+ secret through the callback. A missing, empty or invalid-encoding secret fails
191
+ issuance and rolls back consumption; the hash is never used as the secret.
192
+
193
+ The [reference probe](validation/node-encryption-symmetric.txt) verifies42
194
+ combinations, including AES-GCMKW, without recipient JWKS. The initial symmetric increment
195
+ implemented24 combinations (`dir`/AES-KW × six content methods); the
196
+ [local tests](validation/symmetric-encryption-current.txt) verify hashed-secret
197
+ DCR, initial/refresh issuance, wrong keys and missing-secret rollback.
198
+ [Independent jose verification](validation/symmetric-encryption-jose.txt)
199
+ decrypts48 actual gem tokens and verifies their inner RS256 signatures.
200
+ The standalone verifier takes a disposable vector file exported with
201
+ `CIBA_SYMMETRIC_EXPORT`; synthetic secrets must not be committed.
202
+
203
+ ECDH is now implemented by the CIBA-local adapter described below. The `jwe`1.1.1 `VALID_ALG` list
204
+ names ECDH and AES-GCMKW, but its algorithm dispatcher has no implementations.
205
+ AES-GCMKW is now provided by the CIBA-local adapter described below; ECDH is
206
+ handled separately. Broader authentication/signature combinations remain pending. Existing RSA evidence does not establish those cases.
207
+
208
+ The [three-DB encryption subset](validation/symmetric-encryption-matrix.txt)
209
+ passes10 tests/1,211 assertions on each SQLite/PostgreSQL/MySQL. A subsequent source-review
210
+ found that upstream `oidc.rb#generate_id_token` eagerly calls
211
+ `oauth_application_jwks` before encoding. The [regression reproduction](validation/symmetric-jwks-red.txt)
212
+ confirmed that an unrelated503 JWKS URI incorrectly prevented symmetric issuance.
213
+ A CIBA-only generation scope now skips that recipient lookup for symmetric
214
+ algorithms and restores the previous scope on success or exception. Authentication
215
+ outside generation still resolves and validates the registered public key.
216
+ The [targeted HTTP tests](validation/symmetric-jwks-current.txt) verify zero
217
+ recipient requests during Basic issuance/refresh and actual private_key_jwt
218
+ retrieval with wrong-key rejection and recovery. The [reference probe](validation/node-symmetric-jwks.txt)
219
+ verifies84 combinations (42 without JWKS and42 with a503 URI), all without
220
+ recipient retrieval. This does not suppress asymmetric encryption or ordinary OP
221
+ key retrieval.
222
+
223
+ The [full local regression](validation/symmetric-encryption-full.txt) passes356 tests/6,261 assertions, with two SQLite row-lock skips.
224
+
225
+ The updated [three-DB subset](validation/symmetric-jwks-matrix.txt) passes21
226
+ tests/1,505 assertions per DB, including encryption and client assertions.
227
+ The [installed artifact](validation/symmetric-encryption-installed.txt) passes16
228
+ symmetric cases across four key algorithms, Basic with hashed storage / POST
229
+ with plaintext storage, and opaque/JWT access tokens. It decrypts and verifies
230
+ initial and refresh ID Tokens while an unrelated JWKS URI is configured and
231
+ any outbound recipient HTTP call would fail the smoke. Existing RSA and TLS
232
+ package checks also pass. The build reuses installed dependencies; network
233
+ resolution of dependencies is not established.
234
+
235
+ [Supported Ruby regression](validation/symmetric-encryption-rubies.txt) passes358 tests/6,274 assertions on each Ruby3.3 and3.4, with two SQLite row-lock skips.
236
+
237
+ ## AES-GCM key wrapping
238
+
239
+ The CIBA-local adapter supports A128GCMKW/A192GCMKW/A256GCMKW. It uses OpenSSL
240
+ AES-GCM to wrap a newly generated content key, with a fresh12-byte random IV,
241
+ empty wrapping AAD and a16-byte authentication tag. Both wrapping parameters
242
+ are included in the protected header, which the JWE backend authenticates as
243
+ content AAD. Content encryption and compact serialization remain in `jwe`;
244
+ there is no global algorithm registration or new token-decryption endpoint.
245
+ The client-secret derivation above also applies to GCMKW.
246
+ [RFC7518 §4.7](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.7).
247
+
248
+ The [registration red test](validation/gcmkw-red.txt) failed before the adapter.
249
+ The expanded [local symmetric tests](validation/gcmkw-current.txt) pass42
250
+ algorithm/content combinations, checking DCR with hashed storage, initial and
251
+ refresh output, wrapping IV/tag lengths and distinct sampled IVs. This sampling
252
+ is not a guarantee of collision-free operation; randomness comes from
253
+ `SecureRandom` for each wrap and from the JWE backend for content keys/IVs.
254
+ The [independent jose verifier](validation/gcmkw-jose.txt) decrypts84 actual gem
255
+ tokens, verifies the inner RS256 signature, rejects wrong keys, and rejects
256
+ changes to wrapping IV/tag, encrypted key, ciphertext and content tag.
257
+
258
+ The [reference execution](validation/node-symmetric-jwks.txt) already covers
259
+ all42 combinations with and without an unrelated503 JWKS URI. The updated [installed-artifact smoke](validation/gcmkw-installed.txt) covers
260
+ all seven symmetric key algorithms in28 configurations: hashed Basic/plaintext
261
+ POST and opaque/JWT access tokens. Initial and refresh tokens are unwrapped,
262
+ decrypted and signature-checked, with unexpected recipient HTTP calls rejected.
263
+ Supported-Ruby validation is recorded separately below.
264
+
265
+ The [GCMKW encryption regression subset](validation/gcmkw-matrix.txt) passes17 tests/1,794 assertions on each SQLite/PostgreSQL/MySQL. The [full local regression](validation/gcmkw-full.txt) passes358 tests/6,724 assertions with two SQLite row-lock skips.
266
+
267
+ The [GCMKW supported-Ruby regression](validation/gcmkw-rubies.txt) passes358 tests/6,724 assertions on each Ruby3.3 and3.4, with two SQLite row-lock skips. The [ECDH contract](research/ecdh-encryption-reference-contract.md) records96 reference configurations and the new adapter with its remaining validation gates.
268
+
269
+ ## ECDH recipient keys
270
+
271
+ ECDH-ES and ECDH-ES+A128KW/A192KW/A256KW accept registered public P-256,
272
+ P-384, P-521 or X25519 recipient keys. All six content methods above are
273
+ supported. Public JWKS/jwks_uri is required; client-secret retrieval is not used.
274
+ For ECDH public recipients, `key_ops` must be omitted or empty, matching the
275
+ reference's public-key import behavior. Nonempty declarations fail issuance; see
276
+ the [metadata comparison](research/ecdh-encryption-reference-contract.md#recipient-key-metadata).
277
+ The protected header includes a fresh public epk, recipient kid when present,
278
+ cty/issuer/audience. Direct ECDH has an empty encrypted-key segment; wrapping
279
+ variants use the existing AES-KW backend. Unsupported curves and invalid key
280
+ agreement fail instead of returning plaintext. See the [reference and current
281
+ validation contract](research/ecdh-encryption-reference-contract.md).
282
+
283
+ This is an initial implementation. [Installed-artifact validation](validation/ecdh-installed.txt)
284
+ now covers32 ECDH configurations. [Ruby3.3/3.4](validation/ecdh-rubies.txt) each pass361 tests/9,074 assertions.
285
+ ECDH-specific HTTP/HTTPS key retrieval, failure/rotation and sequential management
286
+ now pass ([contract](research/ecdh-encryption-reference-contract.md#gem-remote-retrieval-and-management)); real TTL is now [measured](research/ecdh-encryption-reference-contract.md#actual-cache-expiry-without-mutation), with a shorter-max-age policy difference; [management/issuance overlap](research/ecdh-encryption-reference-contract.md#management-update-overlapping-issuance) now has reference and three-DB snapshot evidence.
287
+
288
+ Recipient selection follows explicit alg/use priority and stable input order. Cryptographic validation occurs after selecting the recipient: an unusable preferred key fails issuance without trying another key. [Comparison and regression evidence](research/ecdh-encryption-reference-contract.md#multiple-recipients-and-invalid-preferred-keys).
@@ -0,0 +1,174 @@
1
+ # Optional ID Token hints
2
+
3
+ Enable `ciba_id_token_hint_enabled true` and implement
4
+ `resolve_ciba_id_token_subject(subject, oauth_application:)`. The callback receives
5
+ a cryptographically verified subject (public or enabled pairwise) and the authenticated client record;
6
+ return the corresponding local account ID or nil. Do not assume custom public
7
+ subjects are database primary keys. Normal account eligibility and subsequent
8
+ approval/account/client binding checks still apply. No migration is needed.
9
+
10
+ Exactly one of `login_hint`, enabled `login_hint_token`, or enabled `id_token_hint`
11
+ is accepted. ID Token hints are limited to 8192 bytes. Raw hints are removed before
12
+ request persistence, transaction hooks, device snapshots and observation events.
13
+ Applications must separately redact incoming HTTP parameters in their own logs.
14
+
15
+ The dedicated hint verifier uses the authenticated client's configured ID Token
16
+ signing algorithm, falling back to the OP's first signing algorithm as the issuer
17
+ does. It checks signature, issuer, audience membership and a string subject.
18
+ Like the reference hint verifier, it ignores expiration and does not require iat
19
+ or exp. If present, iat/nbf must be numeric; fractional values are accepted. Future
20
+ nbf is rejected with the configured leeway. Future iat is rejected only when exp
21
+ is absent. A present jti must be a string. Supported algorithm families
22
+ are RS, PS, ES and HS; all twelve256/384/512 variants have actual issuance,
23
+ current/expired reuse and wrong-key rejection tests.
24
+ Unsigned and encrypted hints are not supported. A valid nested encrypted hint
25
+ is rejected before subject resolution, matching the measured node-oidc-provider
26
+ 9.12.2 CIBA behavior even with its encryption feature and OP key configured.
27
+
28
+ Asymmetric keys come from `oauth_jwt_public_keys[algorithm]`, falling back to
29
+ `oauth_jwt_keys[algorithm]` only when no public-key entry exists. Arrays retain
30
+ old verification keys. A supplied kid must match the trusted key's JWT::JWK kid;
31
+ without kid all configured keys for the pinned algorithm may be tried. No token
32
+ URL or embedded key is fetched or trusted. Removing a key stops acceptance of
33
+ hints signed by it. CIBA HMAC issuance and hint verification use the authenticated
34
+ client's secret, matching the reference. See the secret retrieval contract below.
35
+
36
+ Expiration is ignored **only in this dedicated hint verifier**. By default
37
+ `ciba_id_token_hint_max_age nil` leaves age unrestricted while keys remain
38
+ trusted, matching the measured node behavior. Set a positive integer number of
39
+ seconds to limit age from iat. This is separate from expiration and does not
40
+ alter normal access-token or client-assertion validation.
41
+
42
+ The hint path now matches the measured reference's azp and typ handling: neither
43
+ is used to reject a hint. Registered algorithm, trusted key, issuer and client
44
+ audience remain mandatory. The recognized JOSE critical declaration b64=true
45
+ is accepted; unknown critical extensions and unencoded JWT payloads are rejected.
46
+ This change is isolated to hints; access-token, signed-request and client-assertion
47
+ validation is unchanged. With an explicit ciba_id_token_hint_max_age, iat is
48
+ required to evaluate that application age policy. Optional [pairwise subjects](pairwise.md)
49
+ use the same resolver boundary to map the verified subject to a canonical account.
50
+
51
+ The reference even ignores the type of exp in this identity-hint path. This is
52
+ not a contract for issuing malformed ID Tokens or accepting them as authentication
53
+ results. The regression re-signs controlled variants and proves they still only
54
+ create pending requests. [Reference](validation/node-id-hint-validation-profile.txt),
55
+ [three-DB regression](validation/id-hint-validation-matrix.txt).
56
+
57
+ Successful hint validation creates a pending request, not an approval or token.
58
+ New customer authentication/approval remains mandatory. See the
59
+ [reference measurements](research/id-token-hint-reference-contract.md) and
60
+ [local tests](validation/id-token-hint-tests.txt). The later
61
+ [encrypted-hint boundary test](validation/encrypted-hint-boundary.txt) and
62
+ pairwise integration tests extend that evidence. Edwards ID Token support is explicitly configured as described below; arbitrary
63
+ application resolver correctness remains outside the verified scope.
64
+
65
+ ## Complete configured RS/PS/ES/HS matrix
66
+
67
+ The updated test verifies each actually issued ID Token using its configured
68
+ key, issuer and audience, then tests current and expired reuse as hints. Neither
69
+ form inherits approval; a token signed by a different key is rejected before
70
+ request persistence. The HMAC client-secret negative case explicitly selects
71
+ HS256 first, so it tests wrong key ownership rather than only algorithm mismatch.
72
+ [Three databases](validation/id-hint-algorithms-matrix.txt): seven tests /229
73
+ assertions per DB, no failures/errors/skips.
74
+
75
+ The independent reference matrix now covers those twelve plus Ed25519/EdDSA
76
+ ([execution](validation/node-id-hint-algorithms.txt)). Edwards issuance/hint verification was a concrete remaining gap; the initial
77
+ implementation below addresses it separately from request-object/client-assertion verification.
78
+
79
+ ## Explicit Edwards ID Token keys
80
+
81
+ Configure `ciba_id_token_signing_keys` with `"Ed25519"` and/or `"EdDSA"` entries
82
+ containing an OpenSSL Ed25519 private key. Default is an empty hash. The client
83
+ must explicitly select the matching `id_token_signed_response_alg`; the ordinary
84
+ OP default signing algorithm is unchanged. Each entry may instead be an array:
85
+ the first key signs new ID Tokens, and later public/private keys are retained
86
+ for hint verification. The first key must be private and all keys must be Ed25519.
87
+
88
+ The shared JWKS publishes only public OKP material with RFC7638-derived kid, alg
89
+ and sig use; existing upstream keys remain present. Discovery adds the configured
90
+ ID Token algorithms. CIBA issuance signs with the selected key, and hints verify
91
+ with the same configured/retained public keys. Removing a retained key prevents
92
+ acceptance of its hints. Hint issuer/audience/subject/time checks and new-approval
93
+ requirements remain in force. This does not change JWT access-token signing.
94
+
95
+ The gem continues to include at_hash, using the left half of SHA-512 for Edwards.
96
+ The reference's CIBA token-endpoint default omits this optional claim; its
97
+ ID Token model maps both Edwards algorithm names to SHA-512 when hashing is
98
+ requested. This is not a claim that their default claim sets are identical.
99
+
100
+ Edwards ID Tokens can use the optional [recipient encryption](id-token-encryption.md)
101
+ path. Without that enabled capability, an encryption selection raises
102
+ ConfigurationError instead of emitting plaintext and keeps approval available.
103
+ Independent Node verification, installed artifact and supported-Ruby evidence for
104
+ signed output is recorded in the [contract](research/id-token-hint-reference-contract.md).
105
+ Encryption has its own narrower evidence and remaining integration work.
106
+ [Initial14-test three-DB regression](validation/edwards-id-token-matrix.txt),
107
+ [final Edwards/rollback cases](validation/edwards-id-token-final-matrix.txt).
108
+
109
+ ## HMAC client-secret ownership
110
+
111
+ CIBA ID Tokens signed with HS256/HS384/HS512 now use the UTF-8 client secret for
112
+ both issuance and hint verification, as required by
113
+ [OIDC Core ID Token validation](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
114
+ The algorithm is still selected by registered `id_token_signed_response_alg`
115
+ or the upstream default, and must be configured among the supported signing
116
+ algorithms. OP-wide HS keys are not a fallback for a missing client secret.
117
+ Non-CIBA JWT encoding and JWT access-token signing retain upstream key handling.
118
+
119
+ After successful client_secret_basic/client_secret_post authentication, the
120
+ verified secret is retained only on that request's Rodauth instance, bound to
121
+ the authenticated application's ID. This works with hashed DB storage; no new
122
+ plaintext persistence is introduced. With explicitly configured plaintext
123
+ client-secret storage, other authentication methods can use the stored secret.
124
+ For hashed storage plus private_key_jwt/mTLS, supply an application secret lookup:
125
+
126
+ ```ruby
127
+ ciba_id_token_hmac_secret do |oauth_application:|
128
+ secrets.fetch(oauth_application.fetch(:client_id)) # application's secret store
129
+ end
130
+ ```
131
+
132
+ The callback must return that client's original secret, not its password hash
133
+ or an OP signing key. Missing/non-string secrets and insufficient key length
134
+ fail issuance with rollback; they do not produce an ID Token with another key.
135
+ Minimum byte lengths are 32/48/64 respectively, following
136
+ [JWA section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2);
137
+ secret entropy and secure storage remain application responsibilities.
138
+ Current-secret rotation changes both new issuance and accepted HMAC hints.
139
+
140
+ This replaces the unreleased implementation's OP-key HMAC behavior. Previously
141
+ issued OP-key HMAC hints are no longer accepted unless that key is actually the
142
+ client secret. Do not configure a shared OP key as every client's secret to
143
+ preserve those tokens.
144
+
145
+ [Three-DB tests](validation/hmac-id-token-matrix.txt) cover Basic/POST,
146
+ plaintext/hashed storage, all three HS algorithms, initial issuance/refresh,
147
+ current/expired hints, wrong OP key rejection and missing/short callback rollback.
148
+ [Full local regression](validation/hmac-id-token-full.txt) passed353 tests before
149
+ the additional registration-length case below.
150
+
151
+ The installed [HMAC smoke](../examples/hmac_id_token_smoke.rb) exercises all
152
+ three algorithms with plaintext/hashed secret storage and opaque/JWT access
153
+ tokens. It verifies initial issuance, refresh, hints and rejection of the OP key.
154
+ Replacing the stored client secret rejects old authentication and old HMAC hints;
155
+ existing refresh credentials issue with the new secret after new authentication.
156
+ This is a backend replacement fixture, not a standard secret-rotation endpoint
157
+ or a measurement of reference cache propagation.
158
+ [Installed artifact evidence](validation/hmac-id-token-installed.txt),
159
+ [Ruby 3.3/3.4 evidence](validation/hmac-id-token-rubies.txt).
160
+
161
+ ### Dynamically generated secret length
162
+
163
+ Upstream's default generated secret is43 encoded bytes, insufficient for HS384
164
+ and HS512 signing. A [regression](validation/hmac-registration-secret-red.txt)
165
+ reproduced the HS384 failure. For CIBA registration only, the gem now replaces
166
+ an insufficient generated secret with fresh cryptographic randomness sized for
167
+ the selected algorithm, using the upstream secret setter to preserve configured
168
+ hash storage. HS256 and unrelated clients keep their existing generation path.
169
+ This applies whether or not management-token issuance is enabled.
170
+ [Three-DB regression](validation/hmac-registration-secret-matrix.txt) verifies
171
+ registration, hashed storage, authenticated CIBA issuance and client-secret
172
+ signature validation for all three algorithms. The
173
+ [reference registration fixture](validation/node-hmac-registration-secret.txt)
174
+ also produces secrets meeting the algorithm's minimum length.
@@ -0,0 +1,35 @@
1
+ # Optional login_hint_token
2
+
3
+ The default profile continues to accept `login_hint`. Enable `login_hint_token` explicitly and supply an application resolver in the same configuration block:
4
+
5
+ ```ruby
6
+ ciba_login_hint_token_enabled true
7
+ resolve_ciba_login_hint_token do |token, oauth_application:|
8
+ # Your service validates the token and returns its local account ID or nil.
9
+ HintService.resolve(token, client_id: oauth_application[:client_id])
10
+ end
11
+ ```
12
+
13
+ `HintService` is an application example, not a class supplied by the gem. The resolver is required at configuration time when enabled. It receives the already-authenticated OAuth client. Like node-oidc-provider's `processLoginHintToken`, this API does not mandate a token format. It can accept an opaque reference or an application-defined signed token. The application owns authenticity, permitted issuer/audience/client, expiry, replay rules where needed and mapping to the OP's local account. For JWT formats, use appropriate signature/claim validation with trusted keys; decoding alone is not identity verification.
14
+
15
+ The Backchannel Authentication Endpoint requires exactly one supplied `login_hint`, enabled `login_hint_token`, or enabled `id_token_hint`. Empty, non-string, multiple or oversized hints return invalid_request. The token limit is 8,192 bytes, also bounded by the total form-body limit; ordinary login_hint remains limited to 1,024 bytes. This capability adds no custom Discovery field and needs no DB migration.
16
+
17
+ Return nil for a token that does not identify an eligible local account; the response is unknown_user_id. The gem also checks the resulting account is active. Resolver exceptions use the existing server-error boundary and do not create a request. Configuration does not replace the existing `resolve_ciba_login_hint`, which remains required for the default mechanism.
18
+
19
+ For an expired token, raise `Rodauth::CibaSupport::ProtocolError,
20
+ "expired_login_hint_token"` to return the corresponding HTTP400 response.
21
+ Other unexpected resolver failures produce HTTP500 rather than claiming that
22
+ the customer is unknown. The application must document accepted token formats,
23
+ issuers and age policy for its clients. Signature policy, single-use references
24
+ and discovery-service encrypted tokens are application implementations, not
25
+ built-in token formats provided by this gem.
26
+
27
+ After successful resolution, only the account binding enters the stored request. The raw token is removed before request hooks, persistence, device callbacks and observation events. The incoming Rack request necessarily still contains the submitted parameter; application/request logging must apply its own filtering. The reference OP retains raw hint parameters in its asynchronous request; discarding the value is this gem's deliberate storage adaptation.
28
+
29
+ Approval and saved-consent account/client checks remain unchanged. Token-endpoint parameters cannot re-resolve or replace the subject. Disabling this hint mechanism prevents new hint-token requests but does not invalidate already-resolved pending requests: those continue using their saved identity and ordinary completion/expiry/revocation checks.
30
+
31
+ ## Evidence and scope
32
+
33
+ `test/ciba_hint_token_test.rb` verifies opt-in/resolver requirements, invalid shapes/size, mixed hints, client-aware resolver behavior, active-account and approval identity checks, raw-token exclusion from DB/snapshots/events, poll tampering and completion after deactivation. The complete node HTTP harness checks an application-defined opaque hint, client policy, mixed hints, unknown user and the signed ID Token subject. The installed-gem extension smoke now uses this mechanism with claims/resource/RAR.
34
+
35
+ These tests validate the protocol-to-application boundary, not an arbitrary production token format. The fixture resolver recognizes fixed local test references; it is not a reusable production verifier. [Signed authentication requests](signed-requests.md) and [ID Token hints](id-token-hint.md) have separate opt-in implementations and evidence.
data/docs/max-age.md ADDED
@@ -0,0 +1,65 @@
1
+ # CIBA maximum authentication age
2
+
3
+ The gem accepts `max_age` at the backchannel endpoint as a node-oidc-provider
4
+ extension. This does not add a CIBA Core conformance requirement.
5
+
6
+ An explicit request value overrides client `default_max_age`. Omission or an empty
7
+ string uses the registered default, if any. Signed requests use only their signed
8
+ max_age; an unsigned outer value cannot override or supply it.
9
+
10
+ The accepted maximum age is saved with the request before device dispatch. The
11
+ application receives it through `trigger_ciba_authentication_device` and can read
12
+ it later using `ciba_request`:
13
+
14
+ - `nil`: no maximum age was selected.
15
+ - `0`: require fresh authentication.
16
+ - A positive integer: maximum authentication age in seconds.
17
+
18
+ Node retains positive inputs as strings in request.params; zero becomes its
19
+ internal `prompt=login`. The Ruby API uses an integer, retaining zero explicitly,
20
+ so applications need no second prompt field to distinguish fresh authentication
21
+ from no policy. This is an internal representation adaptation, not an HTTP response
22
+ change. Client metadata changes after acceptance do not alter the saved policy.
23
+ Applications must enforce freshness during authentication; approval does not reject
24
+ an old supplied auth_time automatically. Selected max_age causes auth_time to be
25
+ selected for ID Token output, but does not invent an authentication time.
26
+ Refresh preserves the original authentication claim selection/context and does
27
+ not start a new authentication request or reapply current client defaults.
28
+
29
+ ## Parsing and migration
30
+
31
+ Validation follows the measured Node numeric boundary: a finite, non-negative
32
+ safe integer (at most9007199254740991) after numeric conversion. Integral decimal
33
+ and exponent representations and unsigned hexadecimal/binary/octal representations
34
+ are accepted. Whitespace-only input and negative zero mean zero. Unsupported
35
+ syntax, fractional results, negative numbers, nonfinite numbers and arrays are
36
+ rejected before saving or dispatching. The gem bounds the input to1024 UTF-8 bytes.
37
+
38
+ New request tables include nullable BIGINT `max_age`. Existing development schemas
39
+ must add it explicitly before upgraded workers start:
40
+
41
+ ```ruby
42
+ Rodauth::CibaSupport::Schema.add_request_max_age(db)
43
+ ```
44
+
45
+ Pass `table:` / `column:` and configure `ciba_request_columns[:max_age]` for custom
46
+ storage. Startup requires this column. Existing requests remain NULL; migration
47
+ does not reinterpret them using today's client defaults. This migration is in
48
+ addition to the [authentication_claims migration](authentication-claims.md).
49
+ No refresh-table max_age column is needed.
50
+
51
+ ## Evidence
52
+
53
+ The [pinned reference test](../test/reference/full-op/max-age.mjs) checks request
54
+ overrides, numeric forms, zero-to-login conversion, invalid values and successful
55
+ signed ID Tokens using an application-supplied old authTime. See its
56
+ [execution](validation/node-max-age.txt). Gem tests cover saved/frozen callback
57
+ policy, later metadata changes, signed input isolation, rejected values, custom
58
+ columns and forward migration. No deployment authentication policy is verified.
59
+
60
+ Validation: [Ruby3.3 / SQLite full suite](validation/max-age-ruby33.txt) passes391
61
+ tests /10,418 assertions with two row-lock skips. The [Ruby3.4 three-DB subset](validation/max-age-matrix.txt)
62
+ passes22 tests /1,317 assertions on each of SQLite/PostgreSQL/MySQL. Containers
63
+ reuse existing dependency images and mount current source read-only.
64
+ The [installed artifact smoke](validation/max-age-installed.txt) also passes;
65
+ it reuses locally installed dependencies rather than testing network resolution.