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,567 @@
1
+ # Dynamic CIBA registration: measured reference contract
2
+
3
+ DPoP client metadata now has a [separate measured contract](dpop-reference-contract.md#dynamic-registration-of-proof-required-clients): strict boolean validation when enabled, storage requirements, false/default serialization, issuance enforcement, full-replacement updates and ignored metadata when the feature is disabled. This does not establish nonce or all DPoP deployment combinations.
4
+
5
+ Reference: published `oidc-provider` 9.12.2, Node 24.21.0, complete HTTP OP
6
+ with its development memory adapter. The dedicated
7
+ [harness](../../test/reference/full-op/registration.mjs) and
8
+ [passing output](../validation/node-registration-reference.txt) establish these
9
+ observations, not a general DCR or pairwise conformance claim.
10
+
11
+ ## Measured behavior
12
+
13
+ - Registration is discovered through `registration_endpoint`. This fixture
14
+ configures a fixed initial access token; an incorrect bearer token returns 401.
15
+ - A CIBA-only confidential poll client with `response_types: []` can omit both
16
+ `redirect_uris` and `client_name`. Registration returns 201, generated client
17
+ credentials, and an empty redirect URI array.
18
+ - Missing delivery mode, unsupported `push`, missing ping endpoint, an HTTP
19
+ ping endpoint, and a string instead of a user-code boolean return 400
20
+ `invalid_client_metadata`.
21
+ - Unknown internal-looking fields (`id`, `internal_admin`) are ignored; a
22
+ caller-supplied `client_secret` is replaced with a generated secret.
23
+ - A ping client with an HTTPS endpoint and boolean user-code metadata registers.
24
+ The test does not contact that endpoint or exercise user-code verification.
25
+ - A separate registered client combines ping, an RSA-signed request and required
26
+ user code. Its real signed request is rejected with the application's wrong
27
+ code and accepted with its approved code. This node fixture does not complete
28
+ that request or deliver its ping; those phases have separate harnesses.
29
+ - With RAR enabled, registration accepts a supported type-name array and rejects
30
+ a scalar, non-string elements, empty names and unknown types. A full update
31
+ omitting the type list returns an empty list and subsequent RAR requests are
32
+ rejected. The node fixture provides a resource indicator, as required for RAR.
33
+ - Pairwise plus client-secret-basic is rejected. Successful pairwise registration
34
+ and subject derivation are not established by this fixture.
35
+ - The generated poll-client credentials authenticate a real backchannel request.
36
+ After the application creates consent and completes the request, polling returns
37
+ access and ID tokens. This test checks issuance, not cryptographic verification
38
+ of those tokens (the lifecycle harness separately verifies signatures).
39
+ - With node's registrationManagement enabled, the issued registration bearer
40
+ reads its client and is echoed in the response. A partial update lacking
41
+ `client_id` is rejected; a full metadata update succeeds. A second replacement
42
+ omitting the name removes it and retains the original client secret, followed
43
+ by successful deletion. Default management updates rotate the bearer;
44
+ the previous bearer is then rejected with 401.
45
+ Presenting a bearer for a different client both returns 401 and invalidates
46
+ that bearer: a subsequent request for its own client also returns 401.
47
+ - Deleting a client with issued tokens and a pending CIBA request returns 204.
48
+ Subsequent poll returns 401 invalid_client and UserInfo returns 401
49
+ invalid_token. The node memory adapter still contains the Grant, AccessToken
50
+ and pending request records; record retention is distinct from usability.
51
+
52
+ The app callbacks for context and binding message are explicitly no-ops;
53
+ the composition fixture supplies a fixed user-code validation policy. The first attempted run reached
54
+ registration but failed request creation with the default context callback;
55
+ the corrected fixture now passes the complete registration-to-issuance path.
56
+
57
+ ## Gem integration work still required
58
+
59
+ The gem now prepends `CibaSupport::Registration`, with
60
+ `ciba_dynamic_client_registration_enabled` defaulting to false. Initial opt-in
61
+ integration supports the measured public-subject poll registration and token
62
+ issuance path. Static-only behavior remains the default.
63
+
64
+ `test/ciba_registration_test.rb` covers generated credentials, real issuance,
65
+ invalid metadata without insertion, application registration authentication,
66
+ ordinary OAuth required fields, valid management updates, grant removal and
67
+ Discovery. A regression first reproduced a partial update to `subject_type`
68
+ being accepted for an existing CIBA client without resending `grant_types`.
69
+ The first repair merged stored metadata while holding the application row. The
70
+ current implementation instead uses node-style complete replacement for CIBA
71
+ management requests, retaining the row lock through validation/persistence
72
+ (SQLite uses an immediate transaction).
73
+
74
+ This is an initial integration, not a release-ready DCR claim. Additional
75
+ client-auth combinations and remaining metadata validation need coverage.
76
+ Full registration management response semantics
77
+ still need comparison with node. Package/Ruby-version coverage has not yet been
78
+ refreshed for this change.
79
+
80
+ ## Registration security review and repairs
81
+
82
+ Regression tests reproduced upstream's fallback accepting arbitrary DB column
83
+ names and its management serializer exposing the registration access-token hash.
84
+ For CIBA registrations, the gem now selects explicit protocol metadata before
85
+ calling upstream validation. Unknown fields are ignored even if a matching DB
86
+ column exists; generated credentials replace caller-supplied create secrets.
87
+ This mirrors the measured node behavior for unknown fields and create secrets.
88
+ Custom metadata is not automatically admitted merely by adding a database column.
89
+
90
+ Management tests verify a wrong bearer cannot update the row and internal fields
91
+ cannot change identifiers/privilege/token hashes. The earlier compatibility
92
+ handling for ignored-fields-only partial updates has been superseded by full
93
+ replacement: a matching `client_id` is now mandatory, and server-issued token/URI
94
+ fields in the body are rejected before persistence.
95
+ CIBA management responses suppress stored credential hashes, include `client_id`,
96
+ and return scalar `token_endpoint_auth_method`. They now echo the authenticated
97
+ presented bearer and include the registration client URI, matching the measured
98
+ node GET behavior. The stored registration verifier is also suppressed on the
99
+ ordinary serializer path, so an older unprefixed client does not expose it after
100
+ removing its CIBA grant. Other ordinary response mapping remains upstream's.
101
+
102
+ ## Initial management credential issuance
103
+
104
+ When CIBA DCR is enabled, `ciba_issue_registration_access_token` defaults to true.
105
+ Registration generates 32 random bytes encoded as a URL-safe bearer with a
106
+ `ciba_reg_` namespace prefix, stores only
107
+ its bcrypt verifier using upstream `password_hash`, and returns the raw bearer
108
+ and management URI inside the original registration transaction. It also stores
109
+ a unique SHA-256 lookup digest using `ciba_registration_token_digest_column`
110
+ (default `ciba_registration_token_digest`). The application table needs both
111
+ the upstream registration verifier column and this explicit additive migration:
112
+
113
+ ```ruby
114
+ Rodauth::CibaSupport::Schema.add_registration_token_digest(db)
115
+ # Custom table/column supported via table: and column: keyword arguments;
116
+ # configure ciba_registration_token_digest_column to match a custom column.
117
+ ```
118
+
119
+ Missing storage raises a configuration error rather than returning an unusable
120
+ management credential. Setting `ciba_issue_registration_access_token false`
121
+ omits issuance. The migration preserves existing rows with null digests. It does
122
+ not reconstruct digests from bcrypt verifiers: prefixed credentials issued by
123
+ the earlier development implementation need reissuance; unprefixed upstream
124
+ management credentials retain their previous authentication path.
125
+
126
+ The app must mount `load_registration_client_uri_routes` after `r.rodauth`, as
127
+ in the integration fixture. Mounting upstream's broad management route before
128
+ the registration POST route consumes that path and returns 404.
129
+
130
+ The gem test uses the issued credentials for GET, PUT and DELETE. Presenting a
131
+ new issued bearer to another client's URI now invalidates the presented bearer,
132
+ matching node; a subsequent request to the original client's URI returns 401.
133
+ The target client's row and the source's OAuth client secret remain unchanged.
134
+ Unknown or malformed bearers cannot revoke any client. Lookup and password-hash
135
+ verification are both required before a mismatch can invalidate credentials.
136
+
137
+ For ordinary management operations, the transaction locks the presented
138
+ credential's source row before validating the target client identifier. A mismatch clears only the source verifier/digest
139
+ and commits that deliberate invalidation before returning 401. It never locks
140
+ the unrelated target row, avoiding opposite cross-client requests locking each
141
+ other's rows. Concurrent updates of one client and opposite-client mismatches
142
+ are tested on separate connections; the latter invalidate only each presented
143
+ credential and leave OAuth secrets and an unrelated client unchanged.
144
+ Digest/verifier fields are omitted from management responses.
145
+ After invalidation, arbitrary unprefixed bearers are also rejected with 401:
146
+ the management-route-only hash matcher treats a missing verifier as a failed
147
+ match instead of passing nil to BCrypt and returning an internal error.
148
+
149
+ Deletion with existing CIBA and ordinary grants is now covered below. This work
150
+ does not establish full management parity.
151
+
152
+ ## Full replacement updates
153
+
154
+ CIBA management PUT requires a matching `client_id`; mismatches or omission
155
+ return `invalid_request` without saving changes. Server-issued management token,
156
+ URI and issuance/secret-expiry fields are forbidden in the request. Omitted
157
+ protocol metadata is reset; explicit metadata is validated again. The client
158
+ identifier, client secret and app-owned internal columns are retained. The
159
+ registration credential is rotated by default as described below. Browser
160
+ response types require redirect URIs. Optional metadata's
161
+ complete default/type behavior still needs the feature-composition tests.
162
+
163
+ The `ciba_reg_` namespace selects this same management policy and safe response
164
+ serialization even after the client removes its CIBA grant. It is not an
165
+ authentication credential by itself: upstream still checks the full bearer
166
+ against the saved verifier before management validation or serialization.
167
+ Older unprefixed bearers rely on the client's current CIBA grant for selection
168
+ of the replacement policy; stored verifier redaction applies regardless.
169
+
170
+ Mounted-route coverage registers under `/identity/register` and verifies that
171
+ the same missing-client-id rejection applies there. Row-lock selection uses
172
+ the route's remaining path, matching upstream's routing rather than assuming
173
+ registration is mounted at the URL root.
174
+
175
+ ## Optional-feature composition and storage
176
+
177
+ The gem test dynamically registers a client combining ping, RS256 request
178
+ signatures (separate client JWKS) and user-code validation. An unsigned request
179
+ and an incorrect code are rejected before request persistence. A correctly
180
+ signed request is completed, its ping dispatch goes through a test transport
181
+ with the registered endpoint/token, and polling returns a verified signed ID
182
+ Token for the newly generated client. This is not a real HTTPS delivery test;
183
+ the separate installed ping smoke supplies that transport evidence.
184
+
185
+ A full update then removes signing keys/algorithm and the notification endpoint,
186
+ switches to poll, and explicitly sets the user-code metadata false. The unsigned
187
+ request is now accepted under that new registration. This exposed and repaired
188
+ the upstream serializer dropping boolean false from the management response.
189
+
190
+ Another regression reproduced registration returning 201 with submitted JWKS
191
+ even though the application table lacked the JWKS column and upstream discarded
192
+ the key material. All accepted mapped CIBA-registration metadata must now have
193
+ storage before registration/update succeeds; missing columns return
194
+ `invalid_client_metadata` without inserting a client. Unknown ignored metadata
195
+ remains ignored. Column presence is not a claim that every optional feature,
196
+ key shape, algorithm or arbitrary schema type has been validated.
197
+
198
+ ## RAR client metadata
199
+
200
+ With `ciba_authorization_details_enabled`, dynamic registration validates
201
+ `authorization_details_types` as an array of nonempty strings from the OP's
202
+ configured types. The array is serialized as JSON into the existing RAR client
203
+ column; create/read/update responses expose an array. Omission defaults to an
204
+ empty array, including full replacement updates. With the feature disabled,
205
+ the metadata is ignored and not persisted or echoed, even if that column exists.
206
+
207
+ The gem composition test registers a client for CIBA and refresh, requests RAR
208
+ read/delete actions for a resource, and saves approval for read only. Initial
209
+ resource token issuance and refresh carry only that approved read action. A
210
+ management update removing the registered type then prevents refresh of the
211
+ old RAR source. The fixed application callback defines action subset semantics;
212
+ this is not evidence of a generic RAR policy engine.
213
+
214
+ ## Management token rotation
215
+
216
+ `ciba_rotate_registration_access_token` defaults to true, matching node's
217
+ `registrationManagement.rotateRegistrationAccessToken` default. A successful
218
+ PUT writes replacement metadata, a fresh verifier and its unique lookup digest
219
+ in one transaction, and responds with the new raw bearer. The old bearer is no
220
+ longer found; replay against either the original or another client cannot
221
+ invalidate the successor. GET does not rotate. Explicit false retains the bearer.
222
+
223
+ A response-serialization exception is injected after the new token is generated
224
+ and the update executes. The test confirms the row rolls back, the attempted
225
+ new bearer is rejected and the original bearer still authenticates. This follows
226
+ the accepted same-DB atomicity policy rather than claiming node's memory adapter
227
+ has equivalent failure behavior. Loss after a committed response is outside
228
+ that guarantee and requires application recovery/re-registration.
229
+
230
+ Two simultaneous PUTs using one bearer run on separate DB connections. Only one
231
+ can commit a new bearer. PostgreSQL/MySQL should reject the other with 401 after
232
+ acquiring the row lock. SQLite may instead time out waiting for its writer lock;
233
+ recognized BusyException (or adapter lock/serialization errors) returns 503
234
+ `temporarily_unavailable` with Retry-After. Retrying that request then returns
235
+ 401. Other database errors are not mislabeled as retryable. This does not claim
236
+ crash recovery or guarantees for arbitrary application overrides.
237
+
238
+ ## Deleting a client with live grants
239
+
240
+ The gem initially failed deletion when an ordinary OAuth grant referenced the
241
+ client: that grant has no CIBA-consent FK to cascade through. After authenticating
242
+ the management credential, CIBA-managed DELETE now removes OAuth grants belonging
243
+ to that client and the client itself within the existing outer transaction.
244
+ The supplied CIBA schema cascades consent, requests, refresh sources and private
245
+ ping delivery rows. Other clients and their grants remain unchanged. Legacy
246
+ unprefixed management credentials for a currently CIBA-enabled client use the
247
+ same cleanup after their own verifier check.
248
+
249
+ The test first proves opaque UserInfo and refresh work, then changes the client
250
+ to ping and creates a pending notification. It adds an ordinary grant to the
251
+ same client and an unrelated client's grant. A wrong bearer leaves all rows
252
+ unchanged. Successful deletion removes the target's credentials and dependent
253
+ records; UserInfo, refresh and pending poll are rejected. Ping data is pending,
254
+ so this test makes no outbound delivery request.
255
+
256
+ Unlike node's measured memory adapter, the SQL implementation removes dependent
257
+ rows using its established FK relationships; both measured OPs reject subsequent
258
+ use of the deleted client's opaque token and poll credentials. This does not
259
+ promise invalidation of externally validated self-contained JWTs before expiry.
260
+
261
+ A second test adds an application-owned restrictive FK. Failure to delete the
262
+ client rolls back the preceding grant cleanup, leaving its management credential
263
+ usable. The gem does not delete arbitrary application dependencies. The application
264
+ must resolve those dependencies or configure its own deletion policy. Custom
265
+ schema cascade policies remain unverified.
266
+
267
+ ### Deletion lock order and refresh race
268
+
269
+ A deterministic PostgreSQL regression held a consent row while DELETE held the
270
+ client row, reproducing the reverse-order timeout. DELETE now verifies the
271
+ credential without a client lock, locks that client's existing consents in ID
272
+ order, then locks the client and revalidates the credential. A matching client
273
+ may therefore wait for a consent without blocking a refresh's parent-client FK
274
+ access. A mismatched URI still never locks the unrelated target's consents.
275
+ The controlled two-connection test passes on PostgreSQL/MySQL; SQLite's writer
276
+ serialization makes that row-lock-specific test inapplicable.
277
+
278
+ An actual concurrent DELETE/refresh test verifies eventual client deletion,
279
+ empty dependent tables and refusal of the old refresh token. Refresh may finish
280
+ before deletion or be rejected after it; recognized contention returns 503 and
281
+ deletion is retried. This exposed SQLite BusyException being reported as 500 by
282
+ the token endpoint: the same narrowly recognized condition now returns 503 there
283
+ as on management routes. Arbitrary DB errors still retain server-error handling.
284
+ These tests do not establish every interleaving with newly created consents,
285
+ ordinary OAuth issuance, external token consumers or custom transaction hooks.
286
+
287
+ Installed rodauth-oauth 1.7.0 requires `redirect_uris` and `client_name` before
288
+ calling `validate_client_registration_params`. Its registration implementation
289
+ also defaults omitted grants and responses to authorization_code/code. Therefore
290
+ removing the static rejection alone cannot implement the measured contract.
291
+
292
+ The complete integration must cover:
293
+
294
+ 1. Explicit opt-in and CIBA-specific required/default metadata without changing
295
+ unrelated OAuth registration behavior.
296
+ 2. Validated delivery mode, notification endpoint, user-code boolean, request
297
+ signing algorithm/key requirements, and supported subject/authentication
298
+ combinations before persistence. Optional capability flags and migrations
299
+ must constrain the accepted metadata.
300
+ 3. Real register/start/complete/poll behavior with generated credentials, plus
301
+ rejection cases that leave no newly registered client.
302
+ 4. Registration management updates: changing grant types, CIBA metadata or
303
+ authentication must not bypass the same checks. Authorization remains the
304
+ registration feature/application's responsibility.
305
+
306
+ Pairwise remains a separate incomplete boundary; rejecting unsupported pairwise
307
+ registration initially is not full alignment with the reference.
308
+
309
+ ## Reproduction
310
+
311
+ From `test/reference/full-op`, after installing the pinned dependencies:
312
+
313
+ ```sh
314
+ docker run --rm --network none \
315
+ --mount "type=bind,src=$PWD,dst=/app,readonly" --workdir /app \
316
+ node:24-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 \
317
+ node registration.mjs
318
+ ```
319
+
320
+ Only loopback listeners are used; no external network or production credentials.
321
+ Source inspection: `lib/helpers/client_schema.js` and
322
+ `lib/actions/registration.js` in the pinned node package, and upstream
323
+ `lib/rodauth/features/oauth_dynamic_client_registration.rb`.
324
+
325
+ ### Dynamically registered private_key_jwt
326
+
327
+ The real node OP accepts a dynamically registered RSA public JWKS for CIBA
328
+ client authentication. Wrong key, wrong audience and replayed assertion are
329
+ rejected. An issuer audience is accepted at both the backchannel and CIBA token
330
+ endpoints. The gem previously accepted issuer only at the backchannel endpoint;
331
+ its CIBA grant token path now also accepts issuer, retaining endpoint audiences.
332
+ Other token grant paths and unrelated clients retain their existing policy.
333
+
334
+ The gem integration also replaces the registered JWKS using authenticated
335
+ management PUT: fresh assertions signed with the removed key are rejected and
336
+ the replacement key works. This is inline JWKS replacement; remote JWKS cache
337
+ invalidation and algorithm metadata validation are separate remaining work.
338
+
339
+ ### Client authentication signing algorithm
340
+
341
+ `token_endpoint_auth_signing_alg` is optional. When supplied, CIBA registration
342
+ checks its string type, the registered JWT authentication method and the OP's
343
+ advertised algorithms. `none`, an asymmetric/HMAC method mismatch, unsupported
344
+ algorithms and use with Basic authentication are rejected before insertion.
345
+ The application must add a nullable String `token_endpoint_auth_signing_alg`
346
+ column, or configure `oauth_applications_token_endpoint_auth_signing_alg_column`
347
+ to its mapped column; requests supplying the field fail if storage is absent.
348
+ Omission preserves the previous assertion algorithm policy.
349
+
350
+ Authentication compares the assertion header with the saved algorithm before
351
+ signature verification. For private_key_jwt the selected algorithm is also
352
+ passed to the upstream decoder. A regression using keys for RS256 and RS512
353
+ proved that previously an RS512-registered client could authenticate with RS256;
354
+ it now rejects RS256 and accepts RS512. The same registered constraint applies
355
+ on CIBA-capable clients' existing protected assertion authentication paths.
356
+
357
+ The real node fixture explicitly enables RS256/RS512 through
358
+ `enabledJWA.clientAuthSigningAlgValues`; RS512 is not a node default. Gem tests
359
+ configure these through upstream `oauth_jwt_keys`, which supplies its existing
360
+ Discovery signing-algorithm list. This does not decouple client-auth algorithms
361
+ from OP signing configuration. HMAC and other algorithm-family combinations
362
+ need further comparative coverage; the RSA test is not proof of all families.
363
+
364
+ ### Inline JWKS registration validation
365
+
366
+ Inline `jwks` must contain a `keys` array of objects. Recognized RSA, EC, OKP
367
+ and AKP entries have the reference provider's structural checks for required
368
+ public fields, forbidden private parameters, optional alg/kid/use strings and
369
+ x5c arrays. Symmetric oct entries are rejected. Like node, unknown key types
370
+ and unsupported curves are not required to pass recognized-key checks; neither
371
+ registration acceptance nor an empty set proves usable authentication keys.
372
+ Actual signature verification remains necessary. This validation applies to
373
+ CIBA registration and management replacement, without changing ordinary OAuth
374
+ registration's upstream policy.
375
+
376
+ The real node and Ruby fixtures cover malformed top-level/entry structure,
377
+ symmetric keys, every forbidden RSA private parameter, malformed RSA public
378
+ fields and metadata, plus acceptance of an empty set. The gem also verifies
379
+ that rejected management replacement leaves both metadata and the current
380
+ management credential intact. These fixtures do not establish operational
381
+ support for every curve or key type, or validate remote JWKS responses.
382
+
383
+ ### Remote assertion keys and management URL replacement
384
+
385
+ A real loopback HTTP fixture exercises dynamic private_key_jwt registration,
386
+ JWT authentication, an upstream max-age cache hit, and authenticated management
387
+ PUT replacing jwks_uri. Registration's URI policy is explicitly relaxed only
388
+ for the two fixture URLs. With the normal outbound address policy, the first
389
+ assertion must fail without contacting loopback. After a test-only address
390
+ allowance, the old URI is fetched once and cached; changing to the new URI
391
+ rejects old-key assertions and accepts the new key, without refetching the old
392
+ URI. Malformed JSON and oversized responses fail authentication without saving
393
+ pending requests.
394
+
395
+ This exposed private_key_jwt using upstream HTTP directly, bypassing the
396
+ outbound controls already used for signed CIBA authentication requests. The
397
+ protected client-assertion lookup now uses the same bounded, address-checked
398
+ transport. Fetch/parse failures become invalid_client. Unrelated client flows
399
+ retain upstream transport behavior.
400
+
401
+ The published node OP is now exercised by `registration-remote.mjs` over real
402
+ HTTP with its memory adapter. It rejects default loopback lookup with 401
403
+ invalid_client without reaching the receiver. A test-only exception for the
404
+ two owned URLs then permits one fetch and cache reuse. Changing the served key
405
+ at the same URL while max-age=3600 is fresh rejects the new kid, still accepts
406
+ the cached old key and performs no fetch. Management PUT changing jwks_uri then
407
+ rejects the old key and accepts the new key after one new-URL fetch. The gem's
408
+ matching test confirms this sequence; no unknown-kid refresh was added.
409
+
410
+ [Node log](../validation/node-registration-remote-reference.txt),
411
+ [gem DB comparison](../validation/registration-remote-cache.txt).
412
+ This establishes the measured fresh-cache and URL-replacement behavior only.
413
+ Node's source applies a minimum 60-second freshness interval, whereas upstream
414
+ Ruby follows its own HTTP cache policy; no-cache/expiry timing, same-URL metadata
415
+ updates, shared cache ownership and cross-process invalidation still require
416
+ separate comparison. Do not infer universal cache equivalence.
417
+
418
+ ## default_max_age validation and persistence
419
+
420
+ The reference OP accepts non-negative safe integer values (0 through
421
+ 9007199254740991), and rejects negative/fractional numbers, strings, booleans,
422
+ arrays, objects and larger numbers. The gem previously returned 201 for -1;
423
+ valid values were echoed but dropped from insertion because upstream's generic
424
+ metadata key did not map to a symbol column. Both boundaries are now explicit.
425
+ Integral JSON numbers such as 1.0 normalize to an integer before storage.
426
+
427
+ To accept this optional metadata, add a nullable BIGINT `default_max_age` column
428
+ to the application's client table. Override
429
+ `oauth_applications_default_max_age_column` if using another column name.
430
+ Without storage, supplying this metadata is rejected. Failed management PUT
431
+ preserves the whole client row, including its management credential; authenticated
432
+ GET returns the stored value, including zero.
433
+
434
+ This is registration metadata support, not implementation of browser OIDC
435
+ reauthentication or a new CIBA parameter. The application still implements
436
+ authentication policy; no claim of automatic freshness enforcement follows.
437
+ [Reference HTTP results](../validation/node-registration-max-age.txt),
438
+ [initial failing test](../validation/registration-max-age-red.txt).
439
+
440
+ The full three-DB suite passes **265 tests** before the final 1.0 normalization;
441
+ the final targeted test passes **59 assertions** on each DB, including missing
442
+ storage, bad values, persisted valid values, management rollback and GET.
443
+ [Full DB log](../validation/registration-max-age-matrix.txt),
444
+ [final change](../validation/registration-max-age-final.txt).
445
+
446
+ ## require_auth_time boolean metadata
447
+
448
+ The pinned reference accepts only JSON booleans for `require_auth_time` and
449
+ returns both true and false from registration and authenticated management GET.
450
+ The gem previously accepted the string `"true"` and echoed it in a201 response.
451
+ CIBA registration now rejects non-boolean values before persistence and maps the
452
+ metadata to an explicit column. Its management serializer also preserves false;
453
+ upstream's generic serializer omits false-valued columns.
454
+
455
+ Applications accepting this metadata must add a nullable boolean
456
+ `require_auth_time` column, or override
457
+ `oauth_applications_require_auth_time_column`. Supplying it without storage is
458
+ rejected. Invalid management replacement leaves the client row and management
459
+ credential unchanged. No new table requirement applies when it is omitted.
460
+ This registration change validates and stores metadata. The separate token-output
461
+ comparison and subsequent implementation are recorded below.
462
+
463
+ Evidence: [reference registration and GET](../validation/node-registration-auth-time.txt),
464
+ [pre-fix failure](../validation/registration-auth-time-red.txt), and
465
+ [Ruby3.4 SQLite/PostgreSQL/MySQL](../validation/registration-auth-time-matrix.txt),
466
+ 2 tests /95 assertions on each backend including default_max_age regression.
467
+ The DB run uses existing test-image dependencies with current lib/test/examples
468
+ mounted read-only; it is not a new image build or a full-suite run.
469
+
470
+ ### Measured auth_time output difference
471
+
472
+ The dedicated `auth-time.mjs` reference fixture checks six configurations:
473
+ require_auth_time omitted/false/true, each with completion authTime omitted/123.
474
+ All initial and refreshed ID Tokens are independently signature-verified.
475
+
476
+ | Completion time | require_auth_time | Node initial/refresh | Current gem initial/refresh |
477
+ |---|---|---|---|
478
+ | omitted | omitted, false or true | claim absent | claim absent |
479
+ | 123 | omitted or false | claim absent | auth_time=123 |
480
+ | 123 | true | auth_time=123 | auth_time=123 |
481
+
482
+ [Reference execution](../validation/node-auth-time.txt),
483
+ [gem characterization:1 test /48 assertions](../validation/auth-time-current.txt).
484
+ The gem test records current behavior, not an alignment pass. Neither implementation
485
+ invents a timestamp when the application omits it, even for require_auth_time=true.
486
+ Node's common grant helper copies source.authTime to available claims; its claim
487
+ selection, including authorization/claims.js, controls output. The existing gem
488
+ id_token_claims emits any supplied context directly. Before changing it, reconcile
489
+ explicit claims requests, scope-selected authentication claims and initial/refresh
490
+ selection together; do not merely discard the persisted authentication context.
491
+ This was an implementation gap, not an accepted adaptation. The subsequent
492
+ [authentication claim selection](../authentication-claims.md) implementation now
493
+ saves request selection and applies it on initial issuance and refresh. NULL legacy
494
+ sources retain their old output contract as a migration boundary.
495
+
496
+ ### Authentication selection correction evidence
497
+
498
+ The extended reference tests cover18 scenarios each with default and explicit
499
+ scope mappings ([default](../validation/node-authentication-claims.txt),
500
+ [scope mapping](../validation/node-authentication-claims-scope.txt)); each verifies
501
+ both signed initial and refreshed ID Tokens. ACR/AMR support is explicitly configured.
502
+ The gem uses stored request selection plus an application scope mapping, without
503
+ changing stored auth_time/acr/amr values or permitting account claims to replace them.
504
+ [Ruby3.4 three-DB tests](../validation/authentication-claims-matrix.txt) pass13 tests
505
+ /951 assertions each, including saved selection across metadata changes, legacy
506
+ migration and existing pairwise/Edwards/RAR refresh cases. The earlier
507
+ auth-time-current.txt is a pre-correction characterization log.
508
+
509
+ ## default_acr_values and authentication defaults
510
+
511
+ The pinned reference accepts arrays of configured ACR strings, rejects other
512
+ values and unsupported strings, and removes duplicates while preserving first
513
+ occurrence order. Empty arrays are valid. Registration and management GET retain
514
+ the normalized array ([reference](../validation/node-registration-acr-defaults.txt)).
515
+
516
+ CIBA registration now follows that contract. Applications accepting this metadata
517
+ add a nullable text `default_acr_values` column (JSON array), or override
518
+ `oauth_applications_default_acr_values_column`, and configure upstream
519
+ `oauth_acr_values_supported`. Missing storage rejects the metadata; invalid
520
+ management replacement preserves the whole client and credential. The metadata
521
+ column does not configure which ACRs the OP supports.
522
+
523
+ At request acceptance, omitted acr_values uses the client's defaults in order;
524
+ explicit acr_values takes precedence. The selected ACR is exposed to the device
525
+ callback through the ordinary request snapshot. An empty default list makes no
526
+ selection. Completion still supplies the achieved ACR, which is not inferred
527
+ from the requested values. Selection survives later client metadata changes and
528
+ refresh. Static registrations store the same JSON array representation.
529
+
530
+ The reference also selects auth_time when default_max_age is configured, including
531
+ zero; the gem now does likewise. In the reference test an old supplied authTime
532
+ still produces a token. Neither this output selection nor metadata persistence
533
+ proves freshness enforcement. The reference retains max_age in request.params;
534
+ the gem subsequently added a saved maximum-age input in its device request
535
+ snapshot. See [maximum authentication age](../max-age.md) for request precedence,
536
+ normalized zero semantics and migration. Freshness enforcement remains application-owned.
537
+
538
+ [Reference defaults fixture](../validation/node-authentication-defaults.txt)
539
+ verifies18 initial/refresh configurations, including explicit ACR precedence.
540
+ [Local tests](../validation/authentication-defaults-current.txt) pass2 tests /147
541
+ assertions, including registration rollback, preference order and metadata-change
542
+ isolation. Empty request acr_values initially remained stricter; the subsequent correction
543
+ below treats an empty string like omission.
544
+
545
+ Final related DB validation: [Ruby3.4 matrix](../validation/authentication-defaults-matrix.txt)
546
+ passes17 tests /1,193 assertions on SQLite, PostgreSQL and MySQL, with no
547
+ failures/errors/skips. This is the focused defaults/registration/authentication
548
+ selection subset; the earlier385-test full run predates these defaults changes.
549
+
550
+
551
+ ### Empty ACR request correction
552
+
553
+ The gem now treats acr_values="" like omission, matching the pinned reference's
554
+ request/default assignment. Registered ordered defaults apply when present;
555
+ without defaults, empty input does not select an ACR claim. An explicit nonempty
556
+ value still overrides the defaults. No whitespace trimming or changed validation
557
+ of other parameters is implied.
558
+
559
+ Reference evidence covers24 initial/refresh cases each for
560
+ [ordinary requests without defaults](../validation/node-empty-acr-default.txt)
561
+ and [signed requests with defaults](../validation/node-empty-acr-signed.txt).
562
+ The signed fixture supplies a conflicting unsigned outer ACR; it never replaces
563
+ the signed empty/omitted value or the registered fallback. The gem regression
564
+ covers omitted/empty/explicit input with nil/empty/ordered defaults in both modes.
565
+ [Pre-fix rejection](../validation/empty-acr-red.txt),
566
+ [final related tests](../validation/empty-acr-final.txt). The intermediate
567
+ empty-acr-current.txt has local socket permission errors and is not a passing run.
@@ -0,0 +1,42 @@
1
+ # Authentication request validation inventory
2
+
3
+ This reconciles CIBA-067 against the implemented request parser. It records
4
+ protocol validation, not identical policy defaults in every node deployment or
5
+ an independent conformance certification. Required presence and exclusive hint
6
+ selection are separately covered by the [mandatory inventory](required-parameter-inventory.md).
7
+
8
+ All test references below are in `test/`; each named test is included in the
9
+ [current400-test run](../validation/id-hint-validation-full-ruby33.txt).
10
+
11
+ | Inputs | Implemented boundary | Direct tests |
12
+ |---|---|---|
13
+ | Form and repeated parameters | TLS POST/form, no query parameters, bounded body, encoding and duplicate checks; repeated resource has its defined handling | `ciba_protocol_test.rb:test_ciba_endpoints_require_tls_post_and_form_before_state_changes`, `test_duplicate_parameters_are_rejected`; `ciba_resources_test.rb:test_resource_validation_and_additive_migration` |
14
+ | scope | Required openid, scope syntax and registered permissions; offline_access obeys refresh activation | `ciba_requirements_test.rb:test_scope_syntax_errors_are_invalid_scope`; `ciba_protocol_test.rb:test_expiry_binding_context_and_offline_access` |
15
+ | login_hint / login_hint_token | Exclusive enabled hint, string/size bounds; application resolution and eligible account | `ciba_protocol_test.rb:test_request_validation_and_unknown_parameters`; `ciba_hint_token_test.rb:test_hint_token_input_exclusivity_size_and_unknown_user` |
16
+ | id_token_hint | Signature, configured algorithm, issuer/audience/subject, shape/time checks and account mapping; expired tokens are identity hints | `ciba_id_token_hint_test.rb:test_id_hint_rejects_untrusted_claims_and_signatures_before_subject_resolution`, `test_id_hint_malformed_signed_claims_are_protocol_errors`, `test_id_hint_expired_token_is_only_an_identity_hint` |
17
+ | binding_message / requested_expiry | Application binding-message validator, positive integer expiry and configured cap | `ciba_protocol_test.rb:test_expiry_binding_context_and_offline_access`; `ciba_requirements_test.rb:test_requested_expiry_expires_at_the_exact_boundary` |
18
+ | acr_values | Bounded string, empty/omitted registered defaults; authentication policy remains application-owned | `ciba_authentication_claims_test.rb:test_empty_acr_values_uses_defaults_in_plain_and_signed_requests`, `test_registered_authentication_defaults_apply_before_dispatch` |
19
+ | request / request_uri | Registered signed JWT, key/signature/required claims, inner parameters replace outer; nested/remote request references rejected | `ciba_signed_request_test.rb:test_signed_request_rejects_bad_claims_and_keys_before_persistence`, `test_signed_request_missing_required_fields_cannot_use_outer_values` |
20
+ | user_code | Feature activation, bounded string and mandatory application verifier, before persistence | `ciba_user_code_test.rb:test_user_code_input_and_optional_client_policy`, `test_user_code_requires_explicit_callback_and_checks_before_device_dispatch` |
21
+ | client_notification_token | Required for ping; bearer syntax and size | `ciba_ping_test.rb:test_ping_notification_token_bearer_syntax_and_maximum_length`, `test_signed_ping_required_fields_cannot_be_supplied_outside_signature` |
22
+ | nonce | Optional bounded string, bound to accepted request rather than poll | `ciba_nonce_test.rb:test_nonce_is_optional_and_bounded`, `test_nonce_is_bound_to_start_request_not_poll_and_not_observed` |
23
+ | claims | Enabled JSON object/target shape, separate saved consent; claim values are not identity data | `ciba_claims_test.rb:test_claim_input_validation_and_disabled_consent`, `test_claim_rejection_overrides_scope_and_request_value_is_not_identity_data` |
24
+ | resource | Enabled registered URI targets, duplicate handling, scope and audience separation | `ciba_resources_test.rb:test_resource_validation_and_additive_migration`, `test_resource_scope_and_audience_match_reference_and_id_token_keeps_oidc_context` |
25
+ | authorization_details | Enabled JSON structure, registered types, target and application policy | `ciba_rar_test.rb:test_rar_request_shape_client_types_and_resource_are_validated` |
26
+ | max_age | Bounded numeric normalization, safe integer range and defaults; authentication freshness is enforced by the application | `ciba_max_age_test.rb:test_max_age_invalid_values_do_not_dispatch_or_persist` |
27
+ | request_context | Enabled bounded string and mandatory application validator before dispatch; signed inner value isolation | `ciba_request_context_test.rb:test_request_context_rejection_precedes_persistence_and_device_dispatch`, `test_request_context_signed_input_replaces_outer_context` |
28
+ | Unknown input | Ignored; cannot write server-side state | `ciba_protocol_test.rb:test_login_hint_is_application_defined_and_unknown_fields_cannot_set_state` |
29
+
30
+ Client authentication is processed independently before these request values.
31
+ Content-specific interpretation of opaque hints, authentication strength,
32
+ claims, resources, rich authorization details and request context belongs to
33
+ the configured application callbacks; parser validation does not establish
34
+ production policy correctness. Disabled optional features and unknown fields
35
+ follow their documented handling rather than implicitly enabling a feature.
36
+
37
+ Remaining compatibility choices are explicit: user-code activation (CIBA-013),
38
+ hint retention (CIBA-151). The previous stricter ID-hint profile has now been
39
+ aligned as documented in [the hint policy](../id-token-hint.md), with a three-DB regression and the updated400-test full run above. They are not hidden missing parser
40
+ features, nor are they declared approved by this inventory. Remote key cache
41
+ policy is also still pending. A full Cartesian product of extensions is not
42
+ necessary evidence for this aggregate parameter-validation row.
@@ -0,0 +1,39 @@
1
+ # Required CIBA authentication parameters
2
+
3
+ This inventory reconciles CIBA-072 with the enabled implementation. It concerns
4
+ mandatory presence and selection, not a blanket pass for all optional parameter
5
+ values (CIBA-067), application policy, or every combination of extensions.
6
+
7
+ | Condition | Required input | Direct test evidence |
8
+ |---|---|---|
9
+ | Every authentication request | `scope`, containing `openid` | `ciba_requirements_test.rb:test_scope_syntax_errors_are_invalid_scope`; `ciba_protocol_test.rb:test_request_validation_and_unknown_parameters` |
10
+ | Every authentication request | Exactly one enabled hint | `ciba_hint_token_test.rb:test_hint_token_input_exclusivity_size_and_unknown_user`; `ciba_protocol_test.rb:test_request_validation_and_unknown_parameters` |
11
+ | Client registered for signed requests | `request` JWT | `ciba_signed_request_test.rb:test_signed_request_registration_discovery_and_client_authentication` |
12
+ | Signed request | `iss`, `aud`, `exp`, `iat`, `nbf`, `jti`, and inner scope/hint | `ciba_signed_request_test.rb:test_signed_request_missing_required_fields_cannot_use_outer_values`; `test_signed_request_rejects_bad_claims_and_keys_before_persistence` |
13
+ | Ping delivery | `client_notification_token` | `ciba_ping_test.rb:test_ping_rejects_bad_tokens_and_uses_current_registered_destination` |
14
+ | User-code verifier requires a code | `user_code` | `ciba_user_code_test.rb:test_user_code_requires_explicit_callback_and_checks_before_device_dispatch` |
15
+ | Signed request with ping and required user code | Required authentication fields must be inside the signature | `ciba_ping_test.rb:test_signed_ping_required_fields_cannot_be_supplied_outside_signature` |
16
+
17
+ Client authentication is independent and precedes request-object processing.
18
+ The signed-request registration test rejects a valid JWT without client
19
+ authentication; the existing authentication audit covers the registered
20
+ credential mechanisms. A JWT does not supply client authentication merely by
21
+ containing `iss` or `client_id`.
22
+
23
+ `binding_message`, `requested_expiry`, `acr_values`, nonce, claims, resources,
24
+ authorization_details, max_age and request_context are optional protocol inputs.
25
+ Their application policies or content validation must not be confused with an
26
+ unconditional presence requirement. A configured application may impose further
27
+ policy through the documented callbacks. Push remains unsupported in both the
28
+ comparison target and gem.
29
+
30
+ The route validates transport and client authentication, processes the signed
31
+ request, validates scope/hints, validates ping parameters, resolves the account
32
+ and invokes application verifiers before inserting a pending request. The
33
+ signed omission and combined-feature regressions assert absence of persistence
34
+ and device dispatch on rejection, with successful pending controls.
35
+
36
+ [Current full Ruby 3.3 execution](../validation/alignment-consolidated-ruby33.txt):
37
+ 395 tests, 10,593 assertions, zero failures/errors, two SQLite row-lock skips.
38
+ [Focused omission execution](../validation/signed-required-omissions.txt):
39
+ 3 tests, 119 assertions, no failures/errors/skips. Test files are under `test/`.