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,449 @@
1
+ # CIBA mutual TLS: measured reference and unresolved Ruby integration
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
+ ## Real TLS reference
16
+
17
+ Node-oidc-provider 9.12.2 is configured with both `tls_client_auth` and
18
+ `self_signed_tls_client_auth`, certificate-bound access tokens, CIBA poll,
19
+ refresh and introspection. Its TLS listener requests a client certificate.
20
+ Application callbacks obtain the certificate from the TLS socket, use the
21
+ socket's chain-verification result for PKI clients, and compare the subject DN.
22
+ No HTTP header supplies certificate identity. The client also verifies the local
23
+ server certificate. The temporary self-signed test certificates are not
24
+ production credentials. The in-memory adapter and development OP signing keys
25
+ are used; protocol handlers, TLS and cryptography are not stubbed.
26
+
27
+ Run with the installed reference dependencies and Ruby/OpenSSL/Docker:
28
+
29
+ ```sh
30
+ mise exec ruby -- sh bin/test-mtls-reference
31
+ ```
32
+
33
+ The runner creates and removes temporary certificates. It uses the pinned Node
34
+ image with no external network. [Harness](../../test/reference/full-op/mtls.mjs),
35
+ [certificate generator](../../test/reference/full-op/mtls-certificates.rb),
36
+ [reference log](../validation/node-mtls-reference.txt).
37
+
38
+ Measured outcomes for both client authentication methods:
39
+
40
+ - CIBA initiation, approved token collection and confidential refresh succeed
41
+ with the registered certificate.
42
+ - Missing certificates fail client authentication. Supplying certificate/verify
43
+ HTTP headers without a TLS certificate also fails.
44
+ - Introspection reports `cnf["x5t#S256"]` equal to base64url SHA256 of the **entire
45
+ certificate DER**, not the JWK thumbprint of its public key.
46
+ - UserInfo requires the bound certificate. A different certificate with the
47
+ same public key is insufficient. Matching certificate returns the customer.
48
+ - Self-signed client authentication likewise rejects a distinct certificate
49
+ with the same public key; registered x5c identifies the certificate.
50
+
51
+ This fixture covers a DN-based PKI callback and static x5c registration. It does
52
+ not cover every SAN policy, remote JWKS rotation, dynamic registration, proxy
53
+ deployment or multi-process replay/lifecycle behavior.
54
+
55
+ ## Upstream Ruby characterization: not a passing support claim
56
+
57
+ The historical `test/reference/upstream_mtls_probe.rb` ran the actual
58
+ gem Rack stack with upstream `oauth_tls_client_auth` enabled. Rack server
59
+ variables model a verified TLS peer; unlike the node fixture this probe does not
60
+ perform a TLS handshake. It is deliberately outside the normal regression
61
+ suite and records current defects rather than codifying them as desired behavior.
62
+ [Observed log](../validation/upstream-mtls-probe.txt).
63
+
64
+ - PKI CIBA initiation/issuance succeeds with matching DN and `SSL_CLIENT_VERIFY`.
65
+ - The stored certificate thumbprint is **not** SHA256 of certificate DER.
66
+ Source inspection shows upstream hashes the public JWK instead.
67
+ - The resulting bound token accesses UserInfo with no certificate (HTTP200).
68
+ Upstream's UserInfo path does not enforce the certificate binding here.
69
+ - Header-only `HTTP_X_SSL_CLIENT_CERT` plus `HTTP_X_SSL_CLIENT_VERIFY=SUCCESS`
70
+ is accepted. This is safe only under an explicit trusted TLS terminator that
71
+ removes/replaces client-supplied headers; there is no such trust configuration
72
+ in this probe. It is not evidence that every deployment is exposed.
73
+ - A registered self-signed x5c client returns HTTP500. Upstream's JWKS iteration
74
+ and comparison use incompatible shapes/values for this registration.
75
+
76
+ ## Required correction boundary
77
+
78
+ Do not advertise CIBA mTLS support based on enabling the upstream feature alone.
79
+ The default gem configuration does not enable mTLS. Implement an explicit trusted
80
+ certificate/verification boundary for CIBA, correct full-certificate identity
81
+ and digest handling, and connect binding enforcement to the actual UserInfo
82
+ entry point, refresh and introspection. Keep other OAuth grant behavior separate.
83
+ Preserve the distinction between PKI chain/subject validation and registered
84
+ self-signed certificate matching. Then replace characterization expectations
85
+ with rejection/positive regression tests and real TLS installed-artifact checks.
86
+ The initial correction below implements part of this boundary; integration work remains.
87
+
88
+ ## Initial CIBA correction
89
+
90
+ The extension now requires an explicit `ciba_tls_client_certificate` callback
91
+ when upstream `oauth_tls_client_auth` is enabled, plus the configured grant
92
+ certificate-thumbprint column. The callback returns the TLS peer certificate as
93
+ an OpenSSL certificate or PEM String, or nil. It must derive that value from a
94
+ trusted server/terminator integration that already proved possession of the
95
+ certificate key. No forwarded certificate or verification header is read by the
96
+ CIBA path. Other OAuth routes retain upstream behavior.
97
+
98
+ For PKI authentication, configure `ciba_tls_client_certificate_authorized?` and
99
+ `ciba_tls_client_certificate_subject_matches?(property, expected)`. Both default
100
+ to false. The first reports actual chain verification; the second checks the
101
+ single registered DN/SAN value. These are application-owned TLS integration
102
+ policies like the reference callbacks. Multiple registered identity properties
103
+ or mixed TLS authentication method lists are rejected. For self-signed clients,
104
+ the first x5c certificate of a registered key must match the full peer certificate
105
+ DER digest; a same-key/different-certificate presentation fails. Remote JWKS uses
106
+ the existing guarded HTTP path, but TLS-specific remote rotation remains untested.
107
+
108
+ Certificate-bound CIBA issuance now stores SHA256(DER) and exposes it in signed
109
+ JWT cnf and opaque introspection. UserInfo verifies the live saved issuance and
110
+ requires the matching certificate. Confidential refresh authenticates again and
111
+ binds the new access token; it does not add public-client refresh-token binding.
112
+ A missing required certificate or simultaneous DPoP binding is rejected before
113
+ upstream's nested persistence transaction. Rejecting the combination later had
114
+ consumed approval; validation was moved before proof reservation/persistence and
115
+ the regression now verifies unchanged approval and zero token/proof rows.
116
+
117
+ The old upstream-only characterization probe has been replaced by
118
+ `test/ciba_mtls_test.rb`; its historical output above is retained as failure
119
+ evidence, not current behavior. The new tests cover both authentication methods
120
+ and opaque/JWT tokens, default/header-only rejection, wrong certificate, wrong
121
+ PKI authorization/subject, configuration checks, issuance-signing rollback,
122
+ UserInfo expiry/revocation, introspection, refresh and DPoP coexistence rejection.
123
+ [Local regression run](../validation/ciba-mtls-current.txt).
124
+
125
+ This is not complete mTLS alignment. DCR metadata/management,
126
+ remote certificate-key rotation, all SAN
127
+ callback policies, aliases/discovery and additional client-method combinations
128
+ still need work. The reference real-TLS fixture does not prove the new Ruby
129
+ callbacks are correctly wired in a deployment.
130
+
131
+ ## Installed gem with a Ruby TLS server
132
+
133
+ `bin/verify-package` now runs the packaged `examples/mtls_smoke.rb` from its
134
+ temporary gem installation. It checks the loaded mTLS module's filesystem path
135
+ against that installation. A WEBrick TLS listener invokes the actual Rack app;
136
+ the requests are sent over HTTPS with Net::HTTP, not Rack mock dispatch. Rack's
137
+ environment helper is used only to adapt the incoming HTTP request. The trusted
138
+ certificate value is assigned from WEBrick's TLS peer certificate after all
139
+ HTTP headers have been mapped, so caller headers cannot populate that value.
140
+
141
+ The listener requests peer certificates and allows self-signed chains to reach
142
+ the application, like the node fixture. For PKI authentication the application
143
+ callback independently verifies the peer against the test trust store and
144
+ checks the registered DN. A foreign certificate with the same DN fails that
145
+ verification. Self-signed authentication matches registered x5c. The HTTP client
146
+ verifies the server's certificate and hostname against a separate local trust
147
+ store. Keys and certificates are generated in memory for each run.
148
+
149
+ All four combinations (PKI/self-signed, opaque/JWT) pass CIBA initiation,
150
+ approved token collection, UserInfo and confidential refresh. Absent certificates
151
+ and forwarded-header-only certificate claims fail authentication; missing and
152
+ same-key/different certificates cannot use bound UserInfo tokens. A new access
153
+ token after refresh still requires its certificate. ID Token signatures,
154
+ issuer/audience and customer subject are checked; JWT access-token cnf and opaque
155
+ introspection cnf match SHA256 of the certificate DER.
156
+ [Installed TLS run](../validation/mtls-installed-tls.txt).
157
+
158
+ This verifies one in-process Ruby TLS adapter and application trust policy with
159
+ static client registration on SQLite/Ruby4.0.6. It does not cover a production
160
+ reverse proxy, intermediate-chain policies, all SAN policies, dynamic certificate
161
+ registration/rotation or multiple application processes. No gem is published and
162
+ no network dependency resolution is performed by this verification.
163
+
164
+ ## Dynamic registration and management
165
+
166
+ The CIBA metadata allowlist now includes the five PKI subject properties and
167
+ `tls_client_certificate_bound_access_tokens`. When the TLS feature is enabled,
168
+ binding accepts only JSON booleans and defaults to false. Explicit properties
169
+ require their configured storage columns; missing storage fails registration.
170
+ PKI clients must supply exactly one nonempty string subject DN/SAN property.
171
+ Other authentication methods discard subject properties, as the reference does.
172
+ Self-signed clients require registered `jwks` or `jwks_uri`. Requiring both DPoP
173
+ and TLS token binding fails registration. With TLS disabled, TLS properties are
174
+ ignored instead of passed through as arbitrary DB fields.
175
+
176
+ Full management replacement resets omitted binding to false and preserves false
177
+ in its response. Invalid updates preserve the saved row and the management
178
+ credential. Changing issuance policy does not remove existing access-token
179
+ binding: the gem regression dynamically registers a self-signed client, issues a
180
+ bound token, disables future binding through management, and verifies certificate
181
+ requirements on the old token while the new token is unbound.
182
+ [Local cases](../validation/mtls-registration-current.txt).
183
+
184
+ The real-TLS node harness now obtains its PKI and self-signed clients through
185
+ authenticated DCR and uses the generated client IDs for the complete CIBA flow.
186
+ It measures boolean validation, one-subject validation, required self-signed keys,
187
+ default false, incompatible dual binding and failed/successful management
188
+ replacement. [Reference run](../validation/node-mtls-registration.txt).
189
+
190
+ The static-registration installed-gem evidence above is historical; the fixture
191
+ now uses DCR as described below. Remote certificate rotation,
192
+ all SAN callback semantics and aliases remain to be aligned. The global binding
193
+ policy/capability distinction is addressed below.
194
+
195
+ ## Installed DCR and management over TLS
196
+
197
+ The packaged TLS fixture now creates both PKI and self-signed clients through the
198
+ actual authenticated registration endpoint. It uses the returned client ID for
199
+ initiation, token collection, introspection and refresh, and checks that ID Token
200
+ audience matches that generated ID. All existing TLS certificate rejection and
201
+ opaque/JWT binding checks still run in all four combinations.
202
+
203
+ An invalid management PUT leaves policy and the credential usable. A successful
204
+ full replacement omitting the binding flag returns false and rotates the
205
+ management credential: the old credential is rejected and the new one can read
206
+ the client. Old access tokens still require their original certificate. A later
207
+ refresh authenticates through mTLS but, under the new policy, issues an unbound
208
+ access token which can use UserInfo without a certificate. This distinguishes
209
+ TLS client authentication from access-token certificate binding.
210
+
211
+ [Installed DCR TLS run](../validation/mtls-dcr-installed-tls.txt). This is the
212
+ temporarily installed artifact on Ruby4.0.6/SQLite using the same explicit TLS
213
+ trust boundary described above. It does not prove certificate rotation or
214
+ arbitrary deployment/trust-policy behavior. Existing package smoke checks also
215
+ pass; no production code changed in this verification increment.
216
+
217
+
218
+ ## Capability versus client binding policy
219
+
220
+ The node TLS harness advertises binding capability true while a client with
221
+ binding false obtains unbound new access tokens. Existing bound tokens still
222
+ require their certificate. This is measured after management replacement and
223
+ refresh, not inferred from metadata alone.
224
+ [Reference run](../validation/node-mtls-capability.txt).
225
+
226
+ The gem previously let upstream's global binding flag override a CIBA client's
227
+ false policy; both opaque and JWT regressions reproduced it.
228
+ [Failing cases](../validation/mtls-capability-red.txt). CIBA issuance and refresh
229
+ now use only client binding metadata. The upstream global setting is not mutated
230
+ and its ordinary OAuth grant handling remains delegated to upstream. Enabling
231
+ `oauth_tls_client_auth` advertises capability true in both metadata documents,
232
+ regardless of that global policy. A prepended metadata method avoids feature
233
+ load order overwriting the value; the OIDC allowlist preserves it.
234
+
235
+ The related mTLS/Discovery suite passes **21 tests / 311 assertions** on each of
236
+ SQLite, PostgreSQL and MySQL, with no failures/errors/skips.
237
+ [Matrix](../validation/mtls-capability-matrix.txt). The installed TLS fixture now
238
+ sets upstream's global policy true and still verifies management replacement to
239
+ false, certificate-free use of new refreshed tokens, and certificate enforcement
240
+ on old tokens in all four PKI/self-signed × opaque/JWT combinations.
241
+ [Package](../validation/mtls-capability-package.txt). This does not establish
242
+ identical configuration APIs, remote rotation, endpoint aliases or arbitrary
243
+ proxy/SAN policy equivalence.
244
+
245
+
246
+ ## Endpoint aliases are application deployment configuration
247
+
248
+ Inspection of the pinned node9.12.2 `lib/actions/discovery.js` finds no automatic
249
+ `mtls_endpoint_aliases` generation. The real TLS fixture now first verifies that
250
+ absence, then adds aliases through application middleware and serves the same
251
+ provider on a second HTTPS port. Discovered aliases for backchannel authentication,
252
+ token, introspection and UserInfo are used in the complete existing CIBA flow.
253
+ Canonical endpoint metadata and issuer remain unchanged; ID Token issuer remains
254
+ canonical too. [Reference run](../validation/node-mtls-aliases.txt).
255
+
256
+ The installed gem fixture follows the same deployment boundary: application
257
+ Discovery middleware publishes the four aliases and two WEBrick TLS listeners
258
+ serve the same Rack application. `base_url` and `authorization_server_url` remain
259
+ pinned to the canonical issuer. Request environments use the actual listener
260
+ origin, while the peer certificate comes from the TLS socket. The client follows
261
+ the discovered alias URLs; absent/header-only/foreign certificates still fail,
262
+ UserInfo still checks certificate binding, and management still changes only
263
+ future issuance policy. The ID Token signature, issuer and audience are verified.
264
+ All PKI/self-signed × opaque/JWT combinations pass.
265
+ [Installed package](../validation/mtls-aliases-package.txt).
266
+
267
+ No gem routing API is needed for this measured deployment. Aliases are not
268
+ created automatically and the application owns both published URLs and actual
269
+ TLS routing. This is evidence for distinct ports and unchanged paths, not for
270
+ arbitrary path rewrites, reverse proxies, alias-only access enforcement, DPoP
271
+ proof URLs on aliases, or certificate rotation. Earlier references to aliases as
272
+ an unimplemented parity feature were too broad: automatic alias routing is not a
273
+ behavior of the pinned reference. Deployment-specific cases remain unverified.
274
+
275
+
276
+ ## Remote x5c rotation and retrieval failures
277
+
278
+ The node TLS fixture now registers a self-signed client with a remote `jwks_uri`.
279
+ Its locally owned HTTP JWKS receiver is explicitly allowed through the provider's
280
+ fetch policy; inbound authentication still uses real TLS peer certificates.
281
+ Both reference and gem tests verify: a cache with `max-age=3600` retains the old
282
+ certificate; a new certificate with the same public key does not force a fetch;
283
+ after forced cache expiry, only the newly published certificate authenticates.
284
+ Tests expire the actual cache directly rather than waiting an hour. This proves
285
+ replacement semantics, not a wall-clock TTL or distributed-cache guarantee.
286
+ [Reference run](../validation/node-mtls-remote.txt).
287
+
288
+ The gem had reused the signed-request key-lookup flag during TLS authentication.
289
+ A remote 503 therefore returned `invalid_request`, incorrectly treating this as
290
+ key lookup after successful client authentication. A distinct TLS lookup flag
291
+ now selects guarded HTTP transport and returns `invalid_client_metadata` for
292
+ retrieval/JSON/structure failure, matching the observed reference response.
293
+ An empty key set is structurally valid but cannot authenticate, so returns
294
+ `invalid_client` with401. Failed requests do not create pending CIBA requests.
295
+ [Regression before fix](../validation/mtls-remote-red.txt).
296
+
297
+ There is a deliberate failure-cache difference: after refreshing a populated
298
+ reference key store fails with503, its new freshness timestamp retains old keys
299
+ and the next authentication can succeed without another fetch. The gem does not
300
+ restore an expired cached key set after failure and continues to reject until a
301
+ successful fetch. This stricter behavior is retained rather than copying stale
302
+ credential acceptance. Neither behavior instantly revokes already cached keys;
303
+ rotation/revocation must account for the configured cache lifetime. Node also
304
+ uses a minimum60-second freshness interval; the gem retains upstream's HTTP
305
+ cache policy, including `no-cache`, without claiming identical TTL behavior.
306
+
307
+ The final gem case checks repeated503 rejection, blocked destinations without
308
+ network requests, absence of pending records on failure and successful recovery.
309
+ [Final three-DB case](../validation/mtls-remote-final.txt). The broader mTLS/remote
310
+ JWKS subset passes17 tests /304 assertions per DB before those final additions.
311
+ [Related cases](../validation/mtls-remote-matrix.txt).
312
+
313
+ This increment does not yet exercise remote certificate rotation in the
314
+ installed artifact, simultaneous issuer processes, or a real HTTPS JWKS rotation
315
+ receiver. The installed package's existing TLS/aliases and other smoke flows
316
+ are separately rerun; outbound TLS transport has separate earlier JWKS evidence.
317
+
318
+
319
+ ## Installed HTTPS certificate rotation and JWT introspection
320
+
321
+ The installed mTLS smoke now runs in a child process whose `SSL_CERT_FILE` trusts
322
+ only the locally generated fixture certificate file in addition to the process's
323
+ normal trust-path behavior. Verification is never disabled. This isolates the
324
+ fixture CA from the parent process and later smoke fixtures. Self-signed clients
325
+ use a dynamically registered HTTPS `jwks_uri`; the real outbound transport reads
326
+ `x5c` from the owned receiver, with only loopback explicitly allowed by the app.
327
+
328
+ Both opaque and JWT flows pass remote cache reuse and certificate mismatch
329
+ without refetch. After publishing a different certificate containing the same
330
+ key and explicitly expiring the cache, the old certificate cannot authenticate,
331
+ while the new one can refresh an existing confidential client's token. The new
332
+ access token's UserInfo and introspection binding refer to the new certificate.
333
+ Previously issued tokens still require the original certificate. Repeated503
334
+ responses produce invalid_client_metadata without adding pending requests;
335
+ restoring the receiver allows authentication again.
336
+
337
+ The expanded fixture exposed a runtime defect: JWT introspection discarded cnf
338
+ because the upstream serializer receives verified claims rather than a grant
339
+ row. The mTLS serializer now resolves the live CIBA issuance, checks client and
340
+ subject provenance plus the saved/claimed certificate digest, then includes
341
+ `cnf.x5t#S256`. Expired or revoked issuance returns inactive. This is scoped to
342
+ CIBA tokens with the TLS feature; unrelated JWT handling stays upstream-owned.
343
+ [Failing installed run](../validation/mtls-remote-installed-tls.txt).
344
+
345
+ The corrected package passes the entire installed smoke suite, including both
346
+ remote-rotation variants. [Final package](../validation/mtls-remote-installed-final.txt).
347
+ The mTLS subset passes **14 tests /307 assertions** on each of SQLite, PostgreSQL
348
+ and MySQL, including opaque/JWT introspection cnf, expiry and revocation.
349
+ [DB regression](../validation/mtls-jwt-introspection-matrix.txt).
350
+
351
+ These tests use forced cache expiry and one application process on Ruby4.0.6;
352
+ they do not establish real-time TTL behavior, distributed invalidation, arbitrary
353
+ proxy trust configuration, or all TLS subject-matching application policies.
354
+
355
+
356
+ ## Pairwise self-signed TLS client authentication
357
+
358
+ The reference TLS harness dynamically registers a pairwise self-signed client
359
+ with remote jwks_uri and verifies CIBA issuance and matching UserInfo subject.
360
+ [Reference](../validation/node-pairwise-mtls.txt). The gem's private_key_jwt-only
361
+ pairwise gate rejected this combination ([regression](../validation/pairwise-mtls-red.txt)).
362
+ It now accepts self_signed_tls_client_auth when the TLS feature is enabled,
363
+ while still requiring remote JWKS and rejecting simultaneous inline keys. Actual
364
+ certificate verification remains in the mTLS authentication boundary.
365
+
366
+ The new gem cases cover opaque/JWT initiation, different-certificate refusal,
367
+ approval, pairwise subject snapshot, canonical stored account, UserInfo and
368
+ refresh. The installed fixture now combines DCR, pairwise self-signed clients,
369
+ HTTPS x5c rotation and mTLS aliases. It exposed a JWT introspection failure:
370
+ upstream tried to look up the pairwise subject as a local account identifier.
371
+ The mTLS serializer now supplies the already-validated issuance's internal
372
+ account ID to that lookup while keeping the public pairwise sub in the response.
373
+ [Local failure](../validation/pairwise-mtls-introspection-red.txt),
374
+ [initial installed failure](../validation/pairwise-mtls-package.txt).
375
+
376
+ The initial related matrix passes30 tests on each DB; final focused and installed
377
+ results are recorded separately after the introspection correction.
378
+ [Related](../validation/pairwise-mtls-matrix.txt),
379
+ [final DB](../validation/pairwise-mtls-final.txt),
380
+ [final package](../validation/pairwise-mtls-package-final.txt).
381
+ Signed-request-only proof of remote-key ownership remains a separate missing
382
+ pairwise path. These checks do not establish arbitrary app identifier policy or
383
+ multi-process key/sector rotation.
384
+
385
+
386
+ The installed pairwise JWT also exceeded Rodauth's default1024-byte parameter
387
+ limit. Its introspection token became empty before CIBA client-auth selection.
388
+ CIBA introspection now permits identified CIBA token input up to8192 bytes;
389
+ other parameters/routes and unrelated tokens retain upstream limits. This
390
+ unverified marker selects parsing/authentication handling only; signature and
391
+ live issuance validation still precede an active response. The final focused
392
+ case uses a254-character pairwise subject and asserts the JWT exceeds1024 bytes.
393
+ The final installed run passes remote certificate rotation and introspection
394
+ with the pairwise subject; no debug tracing remains in the fixture.
395
+
396
+ ### Long JWT introspection security regression
397
+
398
+ The pairwise mTLS JWT fixture now rejects four otherwise plausible token inputs:
399
+ a foreign signing key, an unsigned JWT, an OP-signed changed subject, and an
400
+ OP-signed unknown CIBA issuance identifier. Each authenticated introspection
401
+ returns active=false. Missing and foreign caller certificates return 401 even
402
+ for the valid long token. These checks exercise actual HTTP routes and the
403
+ stored issuance boundary, not only JWT decoding.
404
+
405
+ A separate request-parameter check confirms that the CIBA marker only permits
406
+ up to 8192 bytes at introspection's token parameter. An 8193-byte candidate,
407
+ a non-CIBA 1025-byte string, the token parameter at another route, and an
408
+ oversized client_secret still yield no parameter value. The fabricated marker
409
+ in this parser test is not authenticated; the route tests above establish that
410
+ selecting the larger parsing limit does not authorize a token or caller.
411
+
412
+ [Local regression](../validation/pairwise-mtls-introspection-security.txt):
413
+ 3 tests, 45 assertions, no failures/errors/skips. No runtime change was needed.
414
+ This is not a new three-database or full-suite execution.
415
+
416
+ ## Real TLS aliases with certificate authentication and DPoP tokens
417
+
418
+ The previously unverified combination now has a complete real-TLS flow in the
419
+ pinned reference OP and installed gem. A separate registered client uses either
420
+ `tls_client_auth` or `self_signed_tls_client_auth`, requires DPoP-bound access
421
+ tokens, and explicitly does not require certificate-bound access tokens.
422
+ These are separate responsibilities: the certificate authenticates the client;
423
+ the DPoP key constrains access-token use.
424
+
425
+ Both fixtures discover application-owned aliases on a second TLS listener while
426
+ retaining the canonical issuer. They verify:
427
+
428
+ - initiation, approval and token collection through the alias;
429
+ - rejection of missing client certificates and missing DPoP proofs at collection;
430
+ - rejection of a proof whose htu names the canonical endpoint instead of the alias;
431
+ - signed ID Tokens with canonical issuer/client audience and no inherited cnf;
432
+ - introspection returning exactly the DPoP jkt, with no x5t#S256 binding;
433
+ - UserInfo rejecting a Bearer request even with the certificate, and accepting a
434
+ DPoP proof without a client certificate;
435
+ - refresh requiring both client certificate authentication and DPoP, and retaining
436
+ the exact expected DPoP-key confirmation in the new access token.
437
+
438
+ The gem fixture additionally runs both opaque and JWT access-token modes and keeps
439
+ upstream's global certificate-binding setting enabled, proving the explicit CIBA
440
+ client binding policy remains authoritative. Its four combinations use actual
441
+ WEBrick TLS peer certificates and trusted server certificates, not forwarded
442
+ certificate assertions. UserInfo uses GET in this integration fixture.
443
+
444
+ [Reference execution](../validation/node-mtls-dpop-alias.txt),
445
+ [installed gem execution](../validation/mtls-dpop-alias-installed.txt).
446
+ No runtime change was needed after the earlier DPoP endpoint-URI correction.
447
+ This closes the concrete combined-alias coverage gap. Production reverse proxies,
448
+ cluster nonce/key distribution and arbitrary application trust policies are not
449
+ established by this local integration test.
@@ -0,0 +1,30 @@
1
+ # CIBA lifecycle comparison against node-oidc-provider v9.12.2
2
+
3
+ 2026-09-30. GitHub connector retrieval succeeded after shell DNS and Web cache failures. The earlier source-access limitation is resolved for the start route, grant handler and request model. The initial analysis below used source inspection and an isolated harness; a subsequent [complete HTTP OP test](../../test/reference/full-op/README.md) now verifies the listed lifecycle scenarios with actual signing and the built-in memory adapter on Node24 LTS.
4
+
5
+ The unchanged CIBA handler and source helper now also execute in [an isolated reference harness](../../test/reference/README.md). `node --experimental-vm-modules test/reference/lifecycle.mjs` passes on Node v26.5.0; [log](../validation/node-reference-lifecycle.txt). With non-transactional in-memory persistence, issuance failure leaves consumption intact and the next poll revokes the Grant and returns invalid_grant. Cryptography, actual token persistence and global HTTP middleware are stubbed/absent. This confirms local control-flow behavior only.
6
+
7
+ ## Confirmed ordering
8
+
9
+ - [Start route](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/actions/authorization/ciba.js): save the BackchannelAuthenticationRequest, prepare auth_req_id/expires_in, await triggerAuthenticationDevice. There is no compensating deletion in this function if the trigger throws. The gem now uses the same persistence-before-trigger boundary; application-owned outer transactions remain a Sequel-specific concern.
10
+ - [CIBA grant handler](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/actions/grants/ciba.js): find a client-bound request; check expiry; return authorization_pending when neither grant nor error exists; consume; propagate a saved result error; validate the Grant; issue tokens.
11
+ - [Source helper](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/grant_source.js): consuming an already consumed source revokes its Grant and throws invalid_grant. Grant lookup checks existence, expiry and client identity.
12
+ - [Request model](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/models/backchannel_authentication_request.js): consume invokes adapter.consume and emits consumed. No consumable mixin is needed for this model; the previously guessed mixin path returned 404.
13
+ - [Common issuance](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/grant_common.js): validates the account and Grant/account match, saves the access token, optionally saves a refresh token, then generates the ID Token and response.
14
+
15
+ ## Implemented alignment
16
+
17
+ The gem now checks request expiry before consumed status. Replaying an unexpired consumed approved request revokes its saved CIBA consent and linked OAuth token rows, then returns invalid_grant. A denied result is consumed on first collection (access_denied); a second collection returns invalid_grant. Trusted Ruby completion retries remain idempotent and do not themselves replay token collection.
18
+
19
+ Tests: test_replayed_result_revokes_its_saved_consent_and_tokens, test_denied_result_is_consumed_on_first_collection, test_expiry_precedes_consumed_result_check.
20
+
21
+ Revocation is scoped by the saved consent ID: reusing one consent across multiple requests means replay can revoke all tokens linked to that consent. Offline JWT verification cannot observe a database revocation without an application/resource-server mechanism.
22
+
23
+ ## Deliberate differences and remaining gaps
24
+
25
+ - The node start function omits interval and the CIBA grant handler has no slow_down branch. The gem advertises and enforces an increasing interval. This statement is limited to these functions; it does not prove absence of all application/server rate limiting.
26
+ - node consumes before result-error propagation and token generation. The gem deliberately rolls back consumption and token rows if issuance/signing fails, per [ADR 0004](../adr/0004-retain-atomic-issuance-and-stage-alignment.md). Atomicity of an application's node adapter is not established by reading these functions.
27
+ - node accepts all three hint mechanisms and invokes application user-code validation. Initial gem scope remains login_hint only; broader capabilities are tracked in the accepted [staged roadmap](../alignment-roadmap.md).
28
+ - node persists claims, nonce, resource, params and RAR-related state. The gem now persists nonce and optionally requested/approved/rejected claims. Full claims constraint policy, resource and RAR remain gaps.
29
+ - node propagates OIDC result errors; the gem now supports generic error codes with one-use collection and explicit storage migration (see completion-error-boundaries.md).
30
+ - Complete node HTTP lifecycle scenarios and the gem's SQLite/PostgreSQL/MySQL matrix now pass. Production node adapters, process crashes and application-specific response-loss recovery remain unverified.
@@ -0,0 +1,45 @@
1
+ # node-oidc-provider と rodauth-ciba の比較
2
+
3
+ 後続調査で専用ソースをGitHub connectorから取得できた。最新の処理順序・差分は[ライフサイクル比較](node-ciba-lifecycle.md)を参照。以下は取得前の比較記録。
4
+
5
+ 調査日: 2026-09-30。この比較は整合化作業前のスナップショット。以後の変更は[整合化作業](../node-alignment.md)を参照。結論: アプリが本人確認と承認UIを担当し、内部APIでOPへ結果を渡す分担は共通。node側は既存OIDCの認可モデル・拡張機能を広く利用し、gem側はpoll/login_hintと同一DB内の状態遷移に範囲を絞っている。機能数だけでは適合性・安全性を比較できない。
6
+
7
+ 比較対象は公開時点でlatestと表示された [node-oidc-provider v9.12.2](https://github.com/panva/node-oidc-provider/releases/tag/v9.12.2) と、この作業ツリーのrodauth-ciba 0.1.0。`lib/rodauth/features/ciba.rb` のSHA-256は `f4336c948a135f3682d08eb292408eaaa168edfff13f8702a21c7af46e3d2121`。
8
+
9
+ これはソース・設定・APIの比較であり、相互接続試験ではない。shellからGitHubをcloneできず、Web経由で取得できたタグ固定の一次資料を読んだ。v9.12.2の `actions/authorization/ciba.js` と `actions/grants/ciba.js` は取得できていない。mainのgrantファイルも参照したが、タグとの差を保証できないため、そこだけからv9.12.2の挙動を断定しない。特にhint全種の検証詳細、slow_down、発行失敗後の再試行、JWT認証の全負例は今回の比較では未確定。
10
+
11
+ | 観点 | node-oidc-provider | このgem | 判断 |
12
+ |---|---|---|---|
13
+ | 配送 | poll / ping。既定poll、pushは設定検証で拒否 | pollのみ | poll初版を維持してよい。[設定検証](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/configuration.js#L607) |
14
+ | アプリとの境界 | `triggerAuthenticationDevice` と `backchannelResult` | `ciba_request_accepted` と `approve_ciba_request` / `deny_ciba_request` | 内部APIという方向は共通。どちらも業務用の承認HTTP APIをこの境界で標準化するものではない。[node設定](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/defaults.js#L1116) |
15
+ | 承認結果 | 保存済みGrantまたはエラー。account/client一致を確認 | account一致を確認し、要求を承認/拒否 | nodeのGrantはscope・claim・resourceごとの許可/拒否を表現できる。gemの承認APIにはscopeを減らす引数がない。[結果API](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/provider.js#L239)、[Grant](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/models/grant.js) |
16
+ | hint / user_code | login_hintとlogin_hint_tokenの解決、user_code検証をアプリ用callbackとして提供 | login_hintのみ解決を差替え可能。他は入口で拒否 | 対応範囲の差。nodeのcallbackもアプリ実装が必要で、任意のトークン形式を自動で検証するわけではない。[callback定義](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/defaults.js#L527) |
17
+ | 署名付き要求 | request-object処理をCIBAにも接続 | 明示的に未対応 | JWTクライアント認証とは別機能。初版へ自動追加しない。[処理スタック](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/actions/authorization/index.js#L90) |
18
+ | クライアント登録 / subject | 共通schemaにCIBA metadata・pairwise向け認証条件がある | 静的登録・public subjectのみ。CIBA DCR拒否 | 公開OSSとしての拡張余地だが初期用途の必須条件ではない。[client schema](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/client_schema.js#L133) |
19
+ | Refresh Token | 共通発行処理にRefresh Token生成とpolicy判断がある | offline_accessを除外し、CIBA refreshを発行しない | 長期アクセスが必要になった時点で再検討。nodeの全CIBA設定で必ず発行されるとは言わない。[共通発行処理](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/grant_common.js#L95) |
20
+ | 詳細な認可要求 | CIBA開始スタックにclaims / resource / RARの処理を接続 | CIBA専用処理ではscope等の固定項目を保存 | 業務操作の詳細は現在アプリが保持する必要がある。[処理スタック](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/actions/authorization/index.js#L114) |
21
+ | binding_message | 既定は限定文字集合・1〜20文字。検証callbackあり | 制御文字を除くUTF-8、最大128bytes | ライブラリの方針差。nodeの文字数をCoreの必須条件として移植しない。[検証既定値](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/defaults.js#L546) |
22
+ | 永続化 | モデル単位のadapter。既定in-memory、本番用adapterは導入側 | Sequel、既存OAuthと同じDB | gem側のDB選択は単純で妥当。汎用adapterを追加する理由にはならない。[adapter契約](https://github.com/panva/node-oidc-provider/blob/v9.12.2/example/my_adapter.js) |
23
+ | イベント | Provider event emitter。モデル保存後にsaved等をemit | transaction内のbefore/afterと、outer commit後の例外隔離した観測を分離 | nodeのイベントを「DB commit後・失敗しても認証へ無影響」と同一視しない。[保存・emit](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/models/base_model.js#L71)、[イベント一覧](https://github.com/panva/node-oidc-provider/blob/v9.12.2/docs/events.md) |
24
+ | 適合性の証拠 | READMEにFAPI CIBA認定を記載 | 宣言した範囲のローカルテスト、認定なし | 実績には差がある。ただしnodeの任意設定が認定済みになるわけではない。[README](https://github.com/panva/node-oidc-provider/blob/v9.12.2/README.md#certification) |
25
+
26
+ gem側の根拠は [実装](../../lib/rodauth/features/ciba.rb)、[API契約](../api.md)、[テスト対応表](../protocol-coverage.md)、[直近のレビュー結果](../security-review.md)。最後の実行記録はRuby 4.0.6 / SQLiteで76 tests / 563 assertions。今回はコード変更・テスト再実行をしていない。
27
+
28
+ 特に参考になるのは、nodeが「認証要求」と「許可した内容」を別のモデルで扱う点。gemは受付時に許可候補scopeを保存し、承認時には全体を承認する。これは初版の単純な承認には合う。一部承認が必要なら、まず `approved_scopes` 相当の小さなAPIを検討できるが、その際は受付scopeの部分集合・openid維持・保存後の変更禁止・再試行一致・Token応答のscopeを一緒に検証する必要がある。nodeのGrant体系全体の移植は不要。
29
+
30
+ 今回の「誰が、どの顧客の、何の操作を承認したか」には、scopeだけでは足りない。これは両実装の機能数とは別のアプリ設計問題。現行gemの `after_ciba_request` で、内部request IDに紐付く操作対象・実行者・範囲・期限のスナップショットを同じDBへ保存する案が使える。承認画面と実行側が同じ内容を参照し、後から内容を差し替えられないことを確認する。RARを初版へ追加する前に、この既存フックで足りるかを試すのが妥当。
31
+
32
+ 状態遷移は、単にnodeへ揃えるべきではない。nodeの `backchannelResult` はGrantとのclient/account一致を確認して要求を保存するが、読んだメソッド内にはgemと同じ「同一結果なら冪等、異なる結果ならConflict」という分岐はない。一方gemは期限・完了済み状態・認証contextを確認し、行ロックと状態versionで変更を直列化する。node側の全呼出経路・adapterを含めた競合試験はしておらず、これを脆弱性の認定には使わない。[node結果API](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/provider.js#L239)
33
+
34
+ また、nodeの共通消費処理は消費済みsourceを拒否し、関連Grantのrevokeを呼ぶ。adapterにはfind/upsert/consume等の契約があるが、要求消費と複数トークン保存を一つのDB transactionで確定する保証は、この契約だけからは読み取れない。gemはそれを自前でまとめ、署名失敗時のrollbackと再試行をテストしている。nodeより安全という結論ではなく、保証を置く場所が異なるという比較である。[消費処理](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/helpers/grant_source.js#L21)、[adapter契約](https://github.com/panva/node-oidc-provider/blob/v9.12.2/example/my_adapter.js#L194)
35
+
36
+ 再利用防止でも、nodeの `ReplayDetection.unique` はfindしてからsaveするモデル。gemはclient IDとjtiの組をhash化してunique insertする。nodeのモデルをそのままコピーして、gemのDB unique制約を弱める理由はない。ただしnodeでの同時リクエストの可否はadapter・呼出経路を含めて未検証。[ReplayDetection](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/models/replay_detection.js)
37
+
38
+ 通知失敗の扱いにも注意する。nodeの設定ではAD起動helperを要求受付後・応答前に呼ぶ。gemの受付callbackはcommit後で、例外でも成立済みの受付を変えない。gemでは通知漏れが起きてもpending要求の走査で回復できるが、本番では同一DBへのoutbox保存と配送workerをアプリ側で用意するのが候補になる。観測イベント自体を配送保証とみなさない。pingについてnodeは結果保存後にHTTP通知し、200/204を受理する実装であり、読んだメソッドには永続的な再送queueはない。[ping処理](https://github.com/panva/node-oidc-provider/blob/v9.12.2/lib/models/client.js#L379)
39
+
40
+ 次に行うなら、機能の横並び化より、次の順序を推奨する。
41
+
42
+ 1. 初期アプリで「承認内容の固定」と「通知失敗からの復旧」を既存のtransactional hook・内部IDで確認する。
43
+ 2. gemは現行のpoll/login_hint範囲を維持し、未実施のPostgreSQL/MySQL検証を完了する。
44
+ 3. nodeを動かせる環境で、同じクライアント要求を両OPに送り、開始・拒否・期限・二重引換え・再送を比較する。返却文言の完全一致ではなく、仕様と各実装の宣言した方針に沿って判定する。
45
+ 4. 一部scope承認や別hintの具体的な利用者が現れた場合に、その機能だけを追加検討する。