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,254 @@
1
+ # ID Token hint validation boundary
2
+
3
+ 2026-09-30. Complete node-oidc-provider 9.12.2 HTTP comparison on Node24.21.0, with an actual issued RS256 ID Token and controlled, re-signed test variants. The original comparison established the implementation contract below. An optional gem implementation now exists; see [its validation policy and remaining differences](../id-token-hint.md).
4
+
5
+ ## Measured reference behavior
6
+
7
+ | Input | Result |
8
+ |---|---|
9
+ | Previously issued ID Token | Authentication request accepted with the original account; polling remains authorization_pending until new approval |
10
+ | Correctly signed ID Token expired one hour ago | Accepted as a hint; still needs new approval |
11
+ | Audience array containing the authenticated client and another value | Accepted |
12
+ | Correct audience but azp naming another client | Accepted: this version's helper does not validate azp |
13
+ | Different issuer or audience excluding the current client | invalid_request |
14
+ | Missing subject or future nbf | invalid_request |
15
+ | Modified signature | invalid_request |
16
+
17
+ [Harness](../../test/reference/full-op/lifecycle.mjs), [execution log](../validation/node-full-op-lts.txt). Existing hint-token, resource, claims, RAR and lifecycle scenarios still pass. These tests use one public-subject/RSA configuration; they do not establish HMAC, key rotation, encrypted-hint or pairwise-subject support.
18
+
19
+ `lib/models/id_token.js#validate` chooses the client's configured ID Token signing algorithm, uses the client's symmetric keystore for HS algorithms or the OP keystore otherwise, and verifies the signature, OP issuer, current client audience and string subject while ignoring expiration. `lib/actions/authorization/ciba.js` then resolves the subject as an account and performs the normal new CIBA flow. `lib/helpers/jwt.js#verifyAudience` checks audience membership but does not use the extra ignoreAzp argument passed by its caller. Record that fact rather than assuming azp is checked from the argument name.
20
+
21
+ [CIBA Core §14](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html#SecurityConsiderations) distinguishes a returned ID Token hint from ordinary token use: it discusses accepting expired ID Tokens for a reasonable period while checking issuer, client audience and signature, subject to key retention. This does not make an old ID Token a fresh authentication/authorization result. An implementation needs an explicit hint-validation policy, not globally relaxed access-token verification.
22
+
23
+ ## Upstream integration finding
24
+
25
+ In the installed rodauth-oauth 1.7.0 ruby-jwt path, `jwt_decode(..., verify_claims: false)` is insufficient to accept expired hints. A local probe signed two otherwise identical tokens with the fixture OP key, one expiring in 60 seconds and one expired one hour ago. The former returned claims and the latter nil. `verify_claims: false` supplies an empty options hash to JWT.decode; ruby-jwt's expiration default still applies. RP-initiated logout uses this method too, but that usage is not evidence of the required CIBA hint semantics.
26
+
27
+ Do not change the OP-wide decoder to accept expired tokens. Use a dedicated validation path with explicit signature, algorithm and claim rules and an explicit expiration policy for this parameter only. Do not reuse the backchannel client-authentication assertion ledger: an ID Token hint is not a client assertion and is not a one-use authorization grant.
28
+
29
+ ## Required gem work
30
+
31
+ 1. Add an opt-in capability while preserving default login_hint and optional login_hint_token. Require exactly one hint; do not store raw ID Tokens in request snapshots/events.
32
+ 2. Pin validation to the authenticated client's configured ID Token signing algorithm and trusted OP verification keys (or the correct client symmetric secret for HMAC). Define retained-key/kid behavior. Never select verification keys from untrusted token URLs or silently verify without a key.
33
+ 3. Validate the OP issuer, current client audience membership, subject shape and applicable time claims explicitly. Specify how old an expired hint may be and how retained keys limit acceptance. Decide and test azp semantics rather than mechanically copying the reference's omission.
34
+ 4. Map the validated public subject to an eligible local account through a documented boundary; custom subject encodings cannot be assumed to be account primary keys. Pairwise remains outside the current profile unless its mapping is separately implemented.
35
+ 5. Preserve new customer approval, saved-grant account/client binding, request expiry and one-time result consumption. Poll parameters must not replace the selected subject. A successful hint check alone issues no tokens.
36
+ 6. Test current/expired hints, wrong issuer/client/key/algorithm, malformed/empty subject, future nbf/iat, multiple hints, unknown/closed accounts, stale signing keys and ordinary access-token expiration regression. Include reference and installed-gem evidence before advertisement.
37
+
38
+ The dedicated verifier, HMAC key-source adaptation, retained-key selection and optional maximum age are now implemented and covered by gem integration tests. Pairwise subjects have a separate opt-in implementation and application resolver tests in `test/ciba_pairwise_test.rb`. The requirements above remain review criteria; local passing tests do not establish all node configurations or algorithms.
39
+
40
+ ## Encrypted hint boundary: correction after direct measurement
41
+
42
+ The earlier status treated encrypted hints as a presumed missing parity feature.
43
+ The pinned reference implementation does not decrypt them: `IdToken.validate`
44
+ calls the JWS verifier directly, including in the CIBA route. The presence of
45
+ generic JWT decryption helpers or encrypted ID Token issuance does not establish
46
+ encrypted-hint acceptance.
47
+
48
+ The full HTTP harness now enables node's encryption feature and configures a
49
+ private OP encryption key. It wraps an actual issued RS256 ID Token in a valid
50
+ RSA-OAEP-256/A256GCM JWE with `cty: JWT`, and independently decrypts the envelope
51
+ to verify the fixture. Supplying it as `id_token_hint` returns HTTP400
52
+ `invalid_request` before device dispatch. Its inner signed token is accepted as
53
+ a hint requiring new approval. The complete lifecycle harness still passes.
54
+ [Reference log](../validation/node-encrypted-hint-reference.txt).
55
+
56
+ The gem test similarly constructs and authenticates a real nested JWE using
57
+ OpenSSL, configures an OP encryption key, and checks rejection before subject
58
+ resolution or request creation. The unwrapped signed hint is accepted and stays
59
+ pending. All seven local hint tests pass (100 assertions).
60
+ [Gem log](../validation/encrypted-hint-boundary.txt).
61
+
62
+ Encrypted hints remain unsupported in both measured implementations; this is a
63
+ verified shared boundary, not an outstanding feature needed to match v9.12.2.
64
+ No production decryption code or new gem dependency was added. These tests do
65
+ not cover encrypted ID Token issuance, every encryption algorithm, or upstream
66
+ Ruby's optional JWE backend; they establish the dedicated CIBA hint boundary.
67
+
68
+ ## Actual-issuance algorithm comparison
69
+
70
+ A dedicated reference HTTP fixture now issues and cryptographically verifies
71
+ ID Tokens for RS256/384/512, PS256/384/512, ES256/384/512, HS256/384/512,
72
+ Ed25519 and EdDSA. Current and expired hints create only pending requests;
73
+ wrong-key signatures reject. It is part of the reference npm test command.
74
+ [Fixture](../../test/reference/full-op/id-hint-algorithms.mjs),
75
+ [execution](../validation/node-id-hint-algorithms.txt).
76
+
77
+ The gem's actual-issuance matrix now covers all twelve RS/PS/ES/HS choices,
78
+ including independent signature/issuer/audience verification, forged-signature
79
+ rejection before persistence and pending state for current/expired hints.
80
+ [Three-DB execution](../validation/id-hint-algorithms-matrix.txt): seven tests /229
81
+ assertions per database. At that stage the gem used OP HMAC keys while the
82
+ reference used client secrets. The later correction below changes CIBA issuance
83
+ and hint verification together, rather than conflating those key stores.
84
+
85
+ At this comparison stage, the dedicated hint verifier accepted only those
86
+ twelve algorithms. The initial adapter below subsequently adds Edwards. Edwards request and authentication support does not close
87
+ that gap. Token issuance, published verification keys and hint verification
88
+ must be evaluated together before expanding the accepted hint list.
89
+
90
+ ## Initial Edwards issuance and hint adapter
91
+
92
+ The CIBA extension now has a separate optional Ed25519/EdDSA ID Token key map,
93
+ without registering a global ruby-jwt algorithm or adding signing keys to the
94
+ upstream default map. It publishes public OKP keys, advertises configured ID
95
+ Token algorithms, signs CIBA ID Tokens and reuses the local Edwards signature
96
+ verifier for hints. Client assertions reuse that cryptographic helper while
97
+ retaining their distinct claims/time/replay policy.
98
+
99
+ Registration without configured keys rejects. Issued signatures, public-key
100
+ bytes, at_hash, current/expired hints, wrong key, issuer/audience/subject/nbf and
101
+ retained/removed keys have direct regression evidence. The reference default
102
+ CIBA token response omits at_hash; a proposed test requiring it failed and was
103
+ corrected to assert absence. Its `lib/models/id_token.js` maps Ed25519/EdDSA to
104
+ SHA-512 when calculating optional token hashes. The gem retains its existing
105
+ at_hash inclusion policy and uses that mapping.
106
+
107
+ [Three-DB regression](../validation/edwards-id-token-matrix.txt),
108
+ [final rollback cases](../validation/edwards-id-token-final-matrix.txt).
109
+ This is an initial runtime implementation, not complete interoperability or
110
+ encrypted-token support. An encryption request fails closed rather than silently
111
+ returning a signed plaintext token. Broader integration evidence is recorded
112
+ below; encrypted ID Token issuance remains unsupported.
113
+
114
+ ## Independent Edwards verification of the installed gem
115
+
116
+ `examples/edwards_id_token_smoke.rb` now runs from `bin/verify-package`, issuing
117
+ both algorithm names with opaque and JWT access tokens. Its optional
118
+ `CIBA_EDWARDS_ID_TOKEN_EXPORT` absolute path exports synthetic tokens and public
119
+ JWKS only. The keys are ephemeral and private key material is never exported.
120
+ `test/reference/full-op/verify-gem-edwards-id-tokens.mjs` independently verifies
121
+ all four cases using Node jose: signature, fixed expected issuer/audience/subject,
122
+ JWK thumbprint, SHA-512 at_hash, tampered payload rejection and wrong audience
123
+ rejection. A saved public fixture supports repeatable offline execution; its
124
+ recorded issuance-check time is used, not a claim of current token validity.
125
+
126
+ Reproduce fresh evidence by setting the export path to
127
+ `test/reference/full-op/fixtures/gem-edwards-id-tokens.json` (absolute) when
128
+ running `mise exec ruby -- ruby bin/verify-package`, then run
129
+ `node verify-gem-edwards-id-tokens.mjs` from `test/reference/full-op` with its
130
+ installed jose dependency. The default fixture is explicit historical evidence;
131
+ regenerate it to verify a changed Ruby implementation.
132
+
133
+ [Installed package execution](../validation/edwards-id-token-installed.txt)
134
+ and [independent jose execution](../validation/edwards-id-token-jose.txt) passed.
135
+ This does not establish encrypted ID Token interoperability.
136
+
137
+ ## Edwards refresh and pairwise integration
138
+
139
+ `test_edwards_id_token_refresh_public_subject` and
140
+ `test_edwards_id_token_refresh_pairwise_subject` exercise both Ed25519/EdDSA
141
+ with opaque/JWT access tokens. They verify the actual initial and refreshed
142
+ signatures, stable expected subject, audience, authentication time/method,
143
+ at_hash, UserInfo subject, active signing key replacement with old public key
144
+ retention, refresh rotation/replay rejection and signing-failure rollback.
145
+ Pairwise authentication uses the existing private_key_jwt path; these tests do
146
+ not replace the separate key transport coverage or assert every authentication
147
+ combination. They add integration evidence for the gem, not a new measured
148
+ comparison of these combinations in node-oidc-provider.
149
+
150
+ [Three-DB execution](../validation/edwards-id-token-refresh-matrix.txt) passes
151
+ five Edwards ID Token tests /311 assertions on each of SQLite, PostgreSQL and
152
+ MySQL, including these two new integration cases. The independent jose test
153
+ above covers initial issuance, not these refreshed tokens.
154
+
155
+ [Supported Ruby execution](../validation/edwards-id-token-rubies.txt) passes
156
+ the complete current suite on Ruby 3.3 and 3.4: 341 tests /4,774 assertions each,
157
+ zero failures/errors and two SQLite row-lock skips. This does not cover every
158
+ Ruby/database cross-product; the three-DB Edwards matrix uses Ruby 4.0.6.
159
+
160
+ ## Encrypted output: measured gap and plaintext fallback correction
161
+
162
+ The reference `encrypted-id-tokens.mjs` now performs actual CIBA issuance and
163
+ refresh for RS256, Ed25519 and EdDSA clients using RSA-OAEP-256/A256GCM recipient
164
+ encryption. jose decrypts the compact JWE, checks cty/kid and verifies the inner
165
+ signature, issuer, audience and subject. [Execution](../validation/node-encrypted-id-tokens.txt)
166
+ passes all three algorithms. Encrypted output is supported by the reference;
167
+ the separately measured rejection of encrypted *input hints* does not remove
168
+ this output requirement.
169
+
170
+ Inspection of rodauth-oauth 1.7.0 `oauth_jwt_base.rb` found that the installed
171
+ ruby-jwt backend ignores encryption options when optional JWE support is absent.
172
+ Its optional JWE path can also return the signed token when no recipient key
173
+ matches. The gem previously guarded only the new Edwards branch, so configured
174
+ RSA CIBA clients could receive plaintext despite requesting encryption.
175
+ [Regression before correction](../validation/id-token-encryption-fallback-red.txt)
176
+ reproduces the unexpected 200 response with a signed three-segment ID Token.
177
+
178
+ The CIBA wrapper now requires a five-segment result with the requested alg/enc
179
+ when encrypted ID Token output is configured. Otherwise it raises a configuration
180
+ error inside the existing issuance transaction; no token is returned and the
181
+ request remains approved. The guard is limited to CIBA ID Token encoding and
182
+ does not change unrelated OAuth encoding. Complete and incomplete encryption
183
+ metadata are tested, followed by successful signed issuance after removing that
184
+ configuration. This guard is not an encryption implementation or cryptographic
185
+ verification of a JWE backend. Actual encrypted output, recipient-key selection,
186
+ metadata defaults and independent decryption remain implementation work.
187
+
188
+ [Final regression matrix](../validation/id-token-encryption-fallback-matrix.txt)
189
+ passes seven tests /350 assertions on each of SQLite, PostgreSQL and MySQL,
190
+ including Edwards issuance/refresh/pairwise and existing refresh rollback.
191
+
192
+ The subsequent optional [recipient encryption implementation](../id-token-encryption.md)
193
+ now supplies an initial successful encrypted-output path using jwe 1.1.1.
194
+ The earlier fallback correction remains for disabled/unsupported configurations.
195
+ It covers RSA-OAEP/RSA-OAEP-256 recipient encryption, six AES content methods,
196
+ Edwards/RSA signatures, initial issuance and refresh. DCR and Discovery apply
197
+ only these CIBA ID Token capabilities without widening unrelated algorithms.
198
+ [Three-DB evidence](../validation/id-token-recipient-encryption-matrix.txt):
199
+ nine tests /1,043 assertions each. [Full local regression](../validation/id-token-recipient-encryption-full.txt):
200
+ 345 tests /5,506 assertions, two SQLite row-lock skips.
201
+ [Independent jose decryption](../validation/id-token-recipient-encryption-jose.txt)
202
+ checks 72 issued/refreshed tokens and ciphertext tampering. This does not close
203
+ the broader encryption capability and deployment combinations listed in the
204
+ feature document.
205
+
206
+ The encryption key-selection review subsequently found a concrete difference:
207
+ the gem selected the first eligible key, while the reference ranks matching
208
+ alg/use metadata higher. The correction preserves input order on ties and is
209
+ checked by actual issuance with two different recipient keys.
210
+ [Three-DB regression](../validation/id-token-recipient-priority-matrix.txt)
211
+ passes ten tests /1,047 assertions per database. The final source passes
212
+ [Ruby 3.3/3.4](../validation/id-token-encryption-rubies.txt), 346 tests /5,510
213
+ assertions each (two SQLite row-lock skips), and
214
+ [installed package encryption](../validation/id-token-encryption-installed.txt)
215
+ for initial issuance/refresh with RS256/Ed25519/EdDSA and opaque/JWT access tokens.
216
+ The package probe uses inline keys; it does not establish remote recipient-key
217
+ retrieval or rotation.
218
+
219
+ ## Initial client-secret HMAC alignment
220
+
221
+ CIBA HMAC ID Token issuance and hints now use the client secret, matching
222
+ OIDC Core 3.1.3.7 and the measured reference. Verified Basic/POST secrets can be
223
+ reused within the authenticated request even when stored as hashes. An explicit
224
+ `ciba_id_token_hmac_secret(oauth_application:)` callback supports other methods
225
+ with application-managed original secrets. No OP-key fallback is performed;
226
+ non-CIBA JWT and access-token encoding retain their upstream key sources.
227
+ [Contract and migration boundary](../id-token-hint.md#hmac-client-secret-ownership).
228
+
229
+ [Three-DB regression](../validation/hmac-id-token-matrix.txt) passes nine tests
230
+ /421 assertions each; [full local regression](../validation/hmac-id-token-full.txt)
231
+ passes353 tests /5,829 assertions with two SQLite skips. Independent
232
+ [Node jose verification](../validation/hmac-id-token-jose.txt) verifies24 actual
233
+ gem ID Tokens across three algorithms, Basic/POST, hashed/plaintext storage,
234
+ initial issuance/refresh, rejects the OP key and checks at_hash. Its public
235
+ synthetic fixture records verification time for reproducibility and uses only
236
+ fixed test secrets. Subsequent [installed-artifact validation](../validation/hmac-id-token-installed.txt) covers12 HMAC configurations and backend secret replacement. [Ruby3.3/3.4](../validation/hmac-id-token-rubies.txt) each pass354 tests/5,847 assertions with two SQLite skips. DCR now generates sufficiently long secrets for each HMAC algorithm; the updated [three-DB subset](../validation/hmac-registration-secret-matrix.txt) passes10 tests/439 assertions each. This closes the catalog signature-verification boundary, without claiming every deployment rotation policy or a standard secret-rotation API.
237
+
238
+ ## Hint validation profile alignment
239
+
240
+ The previous azp/typ/required-integer-time restrictions are removed from the
241
+ hint path. The new [reference execution](../validation/node-id-hint-validation-profile.txt)
242
+ accepts fractional times, optional iat/exp, future iat when exp is present,
243
+ non-numeric ignored exp, another azp, an at+jwt typ and the known b64=true
244
+ critical declaration. Missing issuer/audience/subject, wrong signatures,
245
+ future nbf, nonnumeric iat/nbf and non-string jti remain rejected. The gem follows
246
+ these measured boundaries; finite numeric checks remain for evaluated dates.
247
+ An empty string subject is passed to the application resolver rather than being
248
+ inherently interpreted as a database ID. Application age limits require iat.
249
+
250
+ Every accepted test hint remains pending without inherited approval. RSA and
251
+ Edwards paths are covered; client-assertion and signed-request critical-header
252
+ policy is unchanged. [Ruby3.4 three-DB regression](../validation/id-hint-validation-matrix.txt)
253
+ passes31 tests /983 assertions on each database. The initial local broad filter
254
+ hit four sandbox listener errors; that log is not a passing run.