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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/docs/adr/0003-follow-rodauth-hook-conventions.md +9 -0
- data/docs/adr/0004-retain-atomic-issuance-and-stage-alignment.md +16 -0
- data/docs/alignment-closeout.md +136 -0
- data/docs/alignment-roadmap.md +160 -0
- data/docs/api.md +337 -0
- data/docs/authentication-claims.md +80 -0
- data/docs/authorization-details.md +50 -0
- data/docs/claims.md +46 -0
- data/docs/failure-contract.md +34 -0
- data/docs/id-token-encryption.md +288 -0
- data/docs/id-token-hint.md +174 -0
- data/docs/login-hint-token.md +35 -0
- data/docs/max-age.md +65 -0
- data/docs/node-alignment.md +336 -0
- data/docs/operations.md +130 -0
- data/docs/outbound-http.md +52 -0
- data/docs/pairwise.md +76 -0
- data/docs/ping.md +118 -0
- data/docs/protocol-coverage.md +114 -0
- data/docs/refresh-tokens.md +151 -0
- data/docs/release-readiness.md +75 -0
- data/docs/release-validation.md +498 -0
- data/docs/request-context.md +51 -0
- data/docs/requirements-audit-current.json +3221 -0
- data/docs/requirements-audit-current.md +424 -0
- data/docs/requirements-audit.json +3190 -0
- data/docs/requirements-audit.md +175 -0
- data/docs/research/access-token-revocation-reference.md +81 -0
- data/docs/research/alignment-partial-triage.md +50 -0
- data/docs/research/claims-reference-contract.md +33 -0
- data/docs/research/client-assertion-algorithm-contract.md +177 -0
- data/docs/research/completion-error-boundaries.md +56 -0
- data/docs/research/consent-api-evolution.md +59 -0
- data/docs/research/dpop-reference-contract.md +423 -0
- data/docs/research/id-token-hint-reference-contract.md +254 -0
- data/docs/research/mtls-reference-contract.md +449 -0
- data/docs/research/node-ciba-lifecycle.md +30 -0
- data/docs/research/node-oidc-provider-comparison.md +45 -0
- data/docs/research/pairwise-reference-contract.md +264 -0
- data/docs/research/ping-reference-contract.md +165 -0
- data/docs/research/rar-reference-contract.md +55 -0
- data/docs/research/refresh-reference-contract.md +85 -0
- data/docs/research/registration-reference-contract.md +567 -0
- data/docs/research/request-parameter-inventory.md +42 -0
- data/docs/research/required-parameter-inventory.md +39 -0
- data/docs/research/resource-reference-contract.md +40 -0
- data/docs/research/signed-request-reference-contract.md +113 -0
- data/docs/resources.md +55 -0
- data/docs/security-review.md +294 -0
- data/docs/signed-requests.md +162 -0
- data/docs/user-code.md +73 -0
- data/docs/validation/node-alignment-tests.txt +10 -0
- data/examples/client.rb +39 -0
- data/examples/demo_app.rb +121 -0
- data/examples/dpop_smoke.rb +169 -0
- data/examples/ecdh_id_token_smoke.rb +84 -0
- data/examples/edwards_id_token_smoke.rb +48 -0
- data/examples/edwards_smoke.rb +87 -0
- data/examples/edwards_tls_smoke.rb +73 -0
- data/examples/encrypted_id_token_smoke.rb +73 -0
- data/examples/encryption_tls_smoke.rb +123 -0
- data/examples/extensions_smoke.rb +226 -0
- data/examples/fixtures/edwards-requests.json +30 -0
- data/examples/hmac_id_token_smoke.rb +72 -0
- data/examples/migration.rb +18 -0
- data/examples/mtls_smoke.rb +395 -0
- data/examples/ping_smoke.rb +226 -0
- data/examples/registration_smoke.rb +73 -0
- data/examples/server.rb +14 -0
- data/examples/smoke.rb +46 -0
- data/examples/symmetric_id_token_smoke.rb +83 -0
- data/lib/rodauth/ciba/authorization_details.rb +60 -0
- data/lib/rodauth/ciba/dpop.rb +250 -0
- data/lib/rodauth/ciba/http.rb +69 -0
- data/lib/rodauth/ciba/id_token_encryption.rb +175 -0
- data/lib/rodauth/ciba/jwks_cache.rb +149 -0
- data/lib/rodauth/ciba/mtls.rb +174 -0
- data/lib/rodauth/ciba/pairwise.rb +105 -0
- data/lib/rodauth/ciba/registration.rb +489 -0
- data/lib/rodauth/ciba/schema.rb +182 -0
- data/lib/rodauth/ciba/version.rb +6 -0
- data/lib/rodauth/ciba.rb +4 -0
- data/lib/rodauth/features/ciba.rb +2185 -0
- metadata +231 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Pairwise CIBA reference contract
|
|
2
|
+
|
|
3
|
+
Reference: installed node-oidc-provider 9.12.2, compared with rodauth-oauth 1.7.0
|
|
4
|
+
and the current gem. This is an implementation workstream, not a support claim.
|
|
5
|
+
|
|
6
|
+
## Measured behavior
|
|
7
|
+
|
|
8
|
+
`test/reference/full-op/registration-remote.mjs` now enables subjectTypes public
|
|
9
|
+
and pairwise, with an explicit application pairwiseIdentifier callback. The
|
|
10
|
+
callback uses HMAC over a JSON array of sector and account for the fixture; this
|
|
11
|
+
is application policy, not a node default or an algorithm mandated by CIBA.
|
|
12
|
+
|
|
13
|
+
The real registration and CIBA endpoints demonstrate:
|
|
14
|
+
|
|
15
|
+
- Pairwise plus Basic authentication is rejected with invalid_client_metadata.
|
|
16
|
+
- private_key_jwt with only inline JWKS and no jwks_uri is rejected.
|
|
17
|
+
- A CIBA/authorization-code hybrid with response_types code but without
|
|
18
|
+
sector_identifier_uri is rejected.
|
|
19
|
+
- Two CIBA-only private_key_jwt clients with the same jwks_uri are registered,
|
|
20
|
+
authenticated against a real local JWKS server, approved via separate saved
|
|
21
|
+
Grants, and obtain ID Tokens. OP signatures, issuer and client audience are
|
|
22
|
+
verified. Both subjects differ from the internal account ID and match each
|
|
23
|
+
other under the configured same-sector application policy.
|
|
24
|
+
|
|
25
|
+
[Execution log](../validation/node-registration-remote-reference.txt).
|
|
26
|
+
This does not yet test different sectors, hint inversion, hybrid completion or
|
|
27
|
+
sector document validation through HTTP.
|
|
28
|
+
|
|
29
|
+
## Source boundaries that affect the gem
|
|
30
|
+
|
|
31
|
+
`lib/helpers/client_schema.js` requires jwks_uri for pairwise CIBA and an
|
|
32
|
+
explicit sector URI when response_types is nonempty. It permits private_key_jwt
|
|
33
|
+
or self_signed_tls_client_auth for such clients. Self-signed TLS is now implemented and verified separately in the
|
|
34
|
+
[mTLS contract](mtls-reference-contract.md#pairwise-self-signed-tls-client-authentication).
|
|
35
|
+
|
|
36
|
+
`lib/helpers/sector_identifier.js` selects the sector URI's URL.host when given.
|
|
37
|
+
For a CIBA-only client without that URI it selects jwks_uri's URL.host. URL.host
|
|
38
|
+
includes a nondefault port. Hybrid clients use the explicit sector URI because
|
|
39
|
+
registration requires it. `lib/helpers/sector_validate.js` validates a fetched
|
|
40
|
+
array: all redirect URIs (when applicable) and the client's jwks_uri must appear.
|
|
41
|
+
The application's sectorIdentifierUriValidate policy controls this fetch.
|
|
42
|
+
|
|
43
|
+
Node's default pairwiseIdentifier raises until configured. Its CIBA hint code
|
|
44
|
+
(`lib/actions/authorization/ciba.js`) verifies id_token_hint and then passes its
|
|
45
|
+
sub to findAccount. It does not invert a hash itself. The application must map
|
|
46
|
+
that pairwise subject to its canonical account in the authenticated client's
|
|
47
|
+
context; returning the pairwise string as a new account would change identity.
|
|
48
|
+
This source observation has not yet been covered by the fixture's hint flow.
|
|
49
|
+
|
|
50
|
+
Upstream Ruby `oidc.rb#jwt_subject` instead derives a missing sector from
|
|
51
|
+
redirect_uri and hashes host, account ID and its configured secret. A CIBA-only
|
|
52
|
+
client need not have redirect_uri, so removing the gem's public-only guard is
|
|
53
|
+
insufficient. An opt-in CIBA wrapper now supplies the sector/callback boundary; the public-only
|
|
54
|
+
default remains. The initial integration and remaining verification are described
|
|
55
|
+
in [pairwise.md](../pairwise.md).
|
|
56
|
+
|
|
57
|
+
## Implementation requirements
|
|
58
|
+
|
|
59
|
+
1. Opt-in pairwise capability with an explicit application identifier callback,
|
|
60
|
+
leaving the current public default intact. Subject generation must work for
|
|
61
|
+
ID Token, UserInfo, access-token claims and refresh without changing unrelated
|
|
62
|
+
OAuth clients' subject handling.
|
|
63
|
+
2. Validate the pairwise client authentication/URI requirements on static use,
|
|
64
|
+
dynamic registration and management replacement. Derive the CIBA-only sector
|
|
65
|
+
from jwks_uri; handle explicit sectors and hybrid clients with the reference's
|
|
66
|
+
document-membership rules and bounded outbound transport.
|
|
67
|
+
3. Pass the authenticated client and selected sector to identifier generation;
|
|
68
|
+
reject unusable callback results. Avoid pretending a hashing function supplies
|
|
69
|
+
the application's persistence, stable-key lifecycle or identity mapping.
|
|
70
|
+
4. Keep internal account IDs in saved requests/consents. ID Token hint resolution
|
|
71
|
+
must recover that account through the existing application resolver, followed
|
|
72
|
+
by the ordinary account/client/consent checks. A hint must not grant approval.
|
|
73
|
+
5. Verify same-sector stability, different-sector separation, hints, UserInfo,
|
|
74
|
+
refresh, invalid registration and management updates, plus ordinary public
|
|
75
|
+
clients. Compare real node and gem scenarios before advertising support.
|
|
76
|
+
|
|
77
|
+
The approved staged plan still includes this whole boundary. Registration-only
|
|
78
|
+
acceptance is not completion of pairwise alignment.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Refresh integration
|
|
82
|
+
|
|
83
|
+
The real node fixture now requests offline_access for pairwise clients and
|
|
84
|
+
verifies refreshed ID Token signatures, stable subject, matching UserInfo and
|
|
85
|
+
canonical accountId on the saved RefreshToken. offline_access does not become
|
|
86
|
+
a UserInfo attribute.
|
|
87
|
+
|
|
88
|
+
The gem's opaque/JWT integration additionally checks forced refresh rotation,
|
|
89
|
+
old-token replay revoking the successor, refusal to exchange through another
|
|
90
|
+
client, and transaction rollback for nil/empty/non-ASCII/overlong identifier
|
|
91
|
+
callback results. Refresh source and OAuth token counts remain unchanged after
|
|
92
|
+
failed issuance, and a subsequent valid callback can issue successfully.
|
|
93
|
+
|
|
94
|
+
This exposed upstream UserInfo passing offline_access to its generic attribute
|
|
95
|
+
callback and raising when no custom callback exists. CIBA's non-projected account
|
|
96
|
+
claim path now removes that capability scope before delegation, preserving other
|
|
97
|
+
scopes and unrelated OAuth behavior. The fix is shared by opaque/JWT tokens.
|
|
98
|
+
[Reproduction](../validation/pairwise-refresh-red.txt),
|
|
99
|
+
[DB validation](../validation/pairwise-refresh-matrix.txt).
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Real HTTPS sector documents and installed artifact
|
|
103
|
+
|
|
104
|
+
The installed gem reuses the existing TLS fixture in `examples/ping_smoke.rb`.
|
|
105
|
+
A hybrid pairwise client is registered through the real upstream DCR route with
|
|
106
|
+
jwks_uri and a redirect URI in its sector document. Missing either URI, invalid
|
|
107
|
+
JSON/object shape, oversized body, 503 and 302 are rejected without creating a
|
|
108
|
+
client. The untrusted-certificate child process rejects registration before the
|
|
109
|
+
HTTPS application handler receives a request. No registration bearer is sent to
|
|
110
|
+
the document host. All existing installed-package smoke scenarios also pass.
|
|
111
|
+
[Artifact execution](../validation/pairwise-sector-package.txt).
|
|
112
|
+
|
|
113
|
+
The node TLS fixture in `test/reference/full-op/ping.mjs` now measures the same
|
|
114
|
+
membership/JSON/error cases. It **follows** same-origin 302 and accepts a valid
|
|
115
|
+
resulting document; the gem's bounded transport refuses redirects. This is a
|
|
116
|
+
newly measured remaining difference, not evidence of full transport parity.
|
|
117
|
+
A future redirect implementation must validate every destination and bound the
|
|
118
|
+
whole operation; changing the transport globally would also affect ping, whose
|
|
119
|
+
measured contract rejects redirects. Cross-origin redirects were not tested.
|
|
120
|
+
[Node execution](../validation/node-sector-tls-reference.txt).
|
|
121
|
+
|
|
122
|
+
The fixture generates and trusts an ephemeral CA in a child process only. No
|
|
123
|
+
production trust-store modifications or external service calls are involved.
|
|
124
|
+
Installed dependencies are reused; clean network dependency resolution is not
|
|
125
|
+
part of this artifact test.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## Sector redirect implementation
|
|
129
|
+
|
|
130
|
+
The previously measured 302 difference is now repaired in the sector fetch path.
|
|
131
|
+
It follows 301/302/303/307/308 with relative URI resolution, at most 20 redirects,
|
|
132
|
+
and an outer 2.5-second deadline for the entire chain. Each hop calls the existing
|
|
133
|
+
bounded transport with the same address policy and a fresh unauthenticated GET.
|
|
134
|
+
Invalid schemes, credentials in URLs, blocked destinations, loops and timeouts
|
|
135
|
+
are refused. The shared HTTP primitive and ping/JWKS behavior are unchanged.
|
|
136
|
+
|
|
137
|
+
The installed TLS fixture now accepts a valid redirected document and rejects
|
|
138
|
+
loops, file URLs and a redirected address outside its test allowlist without
|
|
139
|
+
client persistence or bearer forwarding. Unit tests cover all five statuses,
|
|
140
|
+
cross-origin dispatch through the policy, exact loop bounds, invalid URI forms
|
|
141
|
+
and an actual elapsed-time timeout spanning repeated stubbed responses.
|
|
142
|
+
[Installed artifact](../validation/sector-redirect-package.txt),
|
|
143
|
+
[DB tests](../validation/sector-redirect-matrix.txt).
|
|
144
|
+
The node same-origin 302 observation remains the comparison evidence. Arbitrary
|
|
145
|
+
cross-origin live TLS deployments, IDN host normalization, sector-document cache
|
|
146
|
+
lifecycle and registration-management effects remain separate work.
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Sector validation lifecycle
|
|
150
|
+
|
|
151
|
+
Source inspection located node sector validation in add_client, Client.validate,
|
|
152
|
+
and the first static-client lookup. Dynamic lookup hashes stored metadata and
|
|
153
|
+
reuses a Client in a 100-entry LRU. The real TLS fixture confirms an unchanged
|
|
154
|
+
loaded client survives a sector endpoint 503 without another fetch; changed
|
|
155
|
+
management metadata fails revalidation, preserving the old client/credential,
|
|
156
|
+
and succeeds when the endpoint recovers.
|
|
157
|
+
|
|
158
|
+
The gem now stores successful validation in a per-Rodauth-class 100-entry LRU
|
|
159
|
+
keyed by a SHA-256 digest of the complete DB client row. A cold lookup fetches;
|
|
160
|
+
warm unchanged metadata does not. Registration and management always validate
|
|
161
|
+
fresh. Failed attempts are never cached or used to invalidate an old success.
|
|
162
|
+
The bounded map uses a mutex only around cache operations, not HTTP. Concurrent
|
|
163
|
+
cold misses may duplicate fetches; no cross-process cache guarantee is made.
|
|
164
|
+
Using the full DB row means app-owned column changes also trigger revalidation,
|
|
165
|
+
unlike a cache keyed only by protocol metadata. Static clients share this bound,
|
|
166
|
+
whereas node maintains a separate static-client map. These are explicit storage
|
|
167
|
+
adaptations, not proof of identical eviction in every deployment.
|
|
168
|
+
|
|
169
|
+
[Node lifecycle evidence](../validation/node-sector-tls-reference.txt),
|
|
170
|
+
[gem DB tests](../validation/sector-cache-matrix.txt),
|
|
171
|
+
[installed TLS regression](../validation/sector-cache-package.txt).
|
|
172
|
+
Tests cover warm reuse, failed refresh, changed metadata, recovery, and eviction
|
|
173
|
+
past 100 entries. Pending/issued token behavior after sector changes is still a
|
|
174
|
+
separate unfinished comparison.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
## Management sector changes and issued subjects
|
|
178
|
+
|
|
179
|
+
The real node fixture now changes a registered pairwise client's jwks_uri to a
|
|
180
|
+
second owned loopback server with a different port (thus a different sector).
|
|
181
|
+
The existing opaque token still obtains UserInfo, whose sub uses the new sector.
|
|
182
|
+
Refresh produces a verified ID Token with that new sub. Old signed tokens are
|
|
183
|
+
not rewritten. This outcome is fixed in the reference assertions.
|
|
184
|
+
|
|
185
|
+
Gem opaque tokens already matched; JWT tokens failed because the original signed
|
|
186
|
+
sub was compared with a recomputation using new metadata. The explicit nullable
|
|
187
|
+
`Schema.add_pairwise_subject` migration now records the issuance subject on each
|
|
188
|
+
new CIBA token row while pairwise is enabled (including public subjects).
|
|
189
|
+
Claims use the saved issuance value; UserInfo authenticates the JWT against that
|
|
190
|
+
value and then emits the current client's pairwise sub. Invalid signed-subject
|
|
191
|
+
substitutions are still refused. The DB/internal account remains unchanged.
|
|
192
|
+
Configuration requires the column before enabling pairwise. Legacy null rows
|
|
193
|
+
use the earlier current-subject comparison and may fail after sector changes;
|
|
194
|
+
there is no automatic historical backfill.
|
|
195
|
+
|
|
196
|
+
The gem tests perform real authenticated management PUT after refresh rotation,
|
|
197
|
+
then test old-token UserInfo, new refresh subject, and replay revocation for both
|
|
198
|
+
opaque and JWT variants. [Initial JWT failure](../validation/pairwise-sector-update-red.txt),
|
|
199
|
+
[DB suite](../validation/pairwise-sector-update-matrix.txt),
|
|
200
|
+
[reference OP](../validation/node-registration-remote-reference.txt).
|
|
201
|
+
The node comparison uses opaque access tokens. Its JWT resource-token profile is
|
|
202
|
+
not a UserInfo token profile, so this is a gem adaptation preserving its existing
|
|
203
|
+
JWT UserInfo capability, not a claim of identical token formats.
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## Pending requests and installed issuance
|
|
207
|
+
|
|
208
|
+
Both real node and gem fixtures now start a request before changing the client's
|
|
209
|
+
sector through authenticated management PUT. The pending record keeps its
|
|
210
|
+
canonical account and no grant; the update does not supply approval. Subsequent
|
|
211
|
+
application approval and token collection produce a verified ID Token using the
|
|
212
|
+
new sector subject. The gem opaque/JWT variants pass on three databases.
|
|
213
|
+
[DB comparison](../validation/pairwise-pending-matrix.txt),
|
|
214
|
+
[node comparison](../validation/node-registration-remote-reference.txt).
|
|
215
|
+
These are sequential transitions, not proof of all concurrent interleavings.
|
|
216
|
+
|
|
217
|
+
The installed-gem TLS fixture now goes beyond registration: a hybrid pairwise
|
|
218
|
+
client authenticates using private_key_jwt against real HTTPS JWKS, begins a
|
|
219
|
+
CIBA request, receives application approval, obtains an issuer/audience/signature
|
|
220
|
+
verified ID Token, persists its issued subject and returns the matching UserInfo
|
|
221
|
+
subject. It explicitly migrates both subject storage and the assertion replay
|
|
222
|
+
ledger. Trusted and untrusted certificate cases remain separate child processes.
|
|
223
|
+
[Artifact evidence](../validation/pairwise-issuance-package.txt).
|
|
224
|
+
This installed pairwise flow uses opaque access tokens; JWT/refresh variants
|
|
225
|
+
are covered in the DB integration suite, not this specific installed example.
|
|
226
|
+
|
|
227
|
+
## Management changes overlapping issuance
|
|
228
|
+
|
|
229
|
+
Two gem tests now cover management PUT during opaque/JWT issuance. PostgreSQL
|
|
230
|
+
and MySQL pause issuance after it loads the client and force management to commit
|
|
231
|
+
before token generation resumes. Both tokens and the stored issuance subject
|
|
232
|
+
retain the old sector, while subsequent UserInfo uses the new sector. Only one
|
|
233
|
+
OAuth grant is created and the request is consumed. SQLite uses simultaneous
|
|
234
|
+
requests without a cross-writer commit barrier, accepts either serial order,
|
|
235
|
+
and checks the same token/storage consistency; a temporary busy response may
|
|
236
|
+
be retried with a fresh client assertion.
|
|
237
|
+
|
|
238
|
+
All three databases pass: SQLite **2 tests / 21 assertions**, PostgreSQL and
|
|
239
|
+
MySQL each **2 tests / 19 assertions**. [Log](../validation/pairwise-concurrent-matrix.txt).
|
|
240
|
+
No runtime change was needed. This validates the gem's same-DB issuance boundary;
|
|
241
|
+
it is not a concurrent reference-OP comparison. Client deletion, authentication
|
|
242
|
+
key replacement, refresh and arbitrary application hooks during these pauses are
|
|
243
|
+
not covered by these two sector-update cases.
|
|
244
|
+
|
|
245
|
+
## Signed requests do not replace pairwise client authentication
|
|
246
|
+
|
|
247
|
+
The pinned 9.12.2 client schema unconditionally restricts pairwise CIBA clients
|
|
248
|
+
to private_key_jwt or self_signed_tls_client_auth. Enabling
|
|
249
|
+
backchannel_authentication_request_signing_alg does not waive that condition.
|
|
250
|
+
A real HTTP DCR regression registers a public-subject Basic client with RS256
|
|
251
|
+
request metadata and remote jwks_uri successfully, then rejects the same
|
|
252
|
+
metadata with pairwise using invalid_client_metadata and the explicit
|
|
253
|
+
client-authentication-method error. The provider explicitly enables both subject
|
|
254
|
+
types so an unsupported-subject rejection cannot satisfy the test.
|
|
255
|
+
|
|
256
|
+
The gem regression accepts the same public configuration and rejects its
|
|
257
|
+
pairwise counterpart without saving a client. No runtime change was needed.
|
|
258
|
+
Earlier notes treated signed-request-only proof as unfinished parity; that
|
|
259
|
+
classification was incorrect. It is a shared restriction of the selected
|
|
260
|
+
reference policy. This registration test does not claim to exercise a signed
|
|
261
|
+
CIBA authentication request or all authentication methods.
|
|
262
|
+
|
|
263
|
+
[Node 24 / pinned provider execution](../validation/node-pairwise-signed-auth-policy.txt),
|
|
264
|
+
[gem: 1 test, 4 assertions](../validation/pairwise-signed-auth-policy.txt).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Ping delivery boundary
|
|
2
|
+
|
|
3
|
+
2026-09-30. Reference: released node-oidc-provider 9.12.2, Node24.21.0,
|
|
4
|
+
complete HTTP OP plus a real HTTPS notification receiver. The gem now supports
|
|
5
|
+
optional [ping](../ping.md) as well as poll; the following evidence defined the
|
|
6
|
+
implementation boundary and still records remaining review criteria.
|
|
7
|
+
|
|
8
|
+
## Measured behavior
|
|
9
|
+
|
|
10
|
+
[Test](../../test/reference/full-op/ping.mjs),
|
|
11
|
+
[full reference log](../validation/node-full-op-lts.txt).
|
|
12
|
+
|
|
13
|
+
| Scenario | Reference outcome |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Missing notification token, spaces in token, >1024 characters | invalid_request |
|
|
16
|
+
| Accepted request before completion | No notification; token request returns authorization_pending |
|
|
17
|
+
| Approval | Save result, then POST notification; token retrieval remains a separate authenticated request |
|
|
18
|
+
| Denial | Same notification shape, followed by access_denied at token endpoint |
|
|
19
|
+
| Notification contents | Bearer client_notification_token; application/json with only auth_req_id |
|
|
20
|
+
| Receiver returns 200 or 204 | Delivery succeeds |
|
|
21
|
+
| Receiver returns 503 | Completion call raises; saved grant binding remains |
|
|
22
|
+
| Receiver returns 302 | Completion call raises; redirect is not followed |
|
|
23
|
+
| Delivery retry | Client.backchannelPing(savedRequest) sends explicitly without recompleting approval |
|
|
24
|
+
| Token retrieval after failed delivery and explicit retry | Succeeds from the saved result |
|
|
25
|
+
| Loopback notification destination under default transport | Rejected by special-use-IP protection before receiver obtains the HTTP request |
|
|
26
|
+
|
|
27
|
+
`Provider.backchannelResult` saves before calling `Client.backchannelPing`.
|
|
28
|
+
The sender accepts 200/204, uses manual redirects and has no built-in durable
|
|
29
|
+
retry queue. Source sets a 2500ms fetch timeout; delayed-network timeout behavior
|
|
30
|
+
was not measured here. The default transport checks the connected socket's IP
|
|
31
|
+
against special-use ranges, rather than relying only on registration syntax.
|
|
32
|
+
|
|
33
|
+
The harness first proves loopback rejection. It then allows exactly its owned
|
|
34
|
+
HTTPS endpoint through a test-only fetch override, retaining certificate
|
|
35
|
+
verification, timeout and redirect policy. A temporary self-signed test CA is
|
|
36
|
+
trusted only by the test process. The override is not production configuration
|
|
37
|
+
or evidence of delivery to a publicly routed endpoint.
|
|
38
|
+
|
|
39
|
+
## Protocol requirements
|
|
40
|
+
|
|
41
|
+
[CIBA Core sections 4, 7.1, 9 and 10.2](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)
|
|
42
|
+
define registered ping mode and an HTTPS client notification endpoint, the
|
|
43
|
+
request's client_notification_token, and notification using that bearer token
|
|
44
|
+
with the auth_req_id. The client subsequently authenticates at the token
|
|
45
|
+
endpoint. Notification does not deliver access or ID tokens.
|
|
46
|
+
|
|
47
|
+
## Gem implementation boundary
|
|
48
|
+
|
|
49
|
+
1. Opt-in ping alongside default poll; explicit client-metadata/storage migration
|
|
50
|
+
and accurate Discovery. Preserve existing poll clients and pending requests.
|
|
51
|
+
2. Validate the registered endpoint and bearer-token syntax/size before request
|
|
52
|
+
persistence. Registered destinations need network-level protection, not just
|
|
53
|
+
an HTTPS prefix check. Do not follow redirects or use request-supplied URLs.
|
|
54
|
+
3. Persist sufficient private delivery data. Current requests store only a hash
|
|
55
|
+
of auth_req_id, which cannot produce the notification body. Both the original
|
|
56
|
+
ID and notification credential must survive completion by another worker.
|
|
57
|
+
Exclude these from all public request APIs, hook snapshots and observations;
|
|
58
|
+
define cleanup/retention and any storage-protection requirements explicitly.
|
|
59
|
+
4. Save approval/denial transactionally, send only after the outermost commit,
|
|
60
|
+
and leave that result intact on delivery failure. Retain the agreed Rodauth
|
|
61
|
+
rollback behavior for before/after state-change hooks. Network delivery must
|
|
62
|
+
not occur while holding the request's DB transaction/row lock.
|
|
63
|
+
5. Provide explicit notification retry, separate from completion; do not add an
|
|
64
|
+
automatic queue or claim exactly-once delivery. A delivery failure does not
|
|
65
|
+
invalidate a saved approval. Handle response loss, duplicate notification,
|
|
66
|
+
expired/consumed requests and concurrent retry explicitly in tests/docs.
|
|
67
|
+
6. Keep token issuance/replay revocation and customer/client binding unchanged.
|
|
68
|
+
Test full acceptance, approval, denial, delivery and token collection using
|
|
69
|
+
an independent TLS receiver and the installed gem, including failure cases.
|
|
70
|
+
|
|
71
|
+
The initial comparison found the gem's registered remote-JWKS helper lacked
|
|
72
|
+
node's special-use-IP guard. The [dedicated CIBA transport](../outbound-http.md)
|
|
73
|
+
now checks DNS answers and pins a validated address; its limits and different
|
|
74
|
+
policy are documented. Unrelated OP HTTP routes remain unchanged. This closes
|
|
75
|
+
part of the transport gap, not ping delivery itself.
|
|
76
|
+
|
|
77
|
+
The research increment itself did not implement ping. The subsequent optional
|
|
78
|
+
implementation adds private delivery storage, after-commit notification and
|
|
79
|
+
explicit retry. Production egress policy and concurrent delivery remain review
|
|
80
|
+
work; the feature documentation records credential retention and failure behavior.
|
|
81
|
+
|
|
82
|
+
## Ignored response body parity
|
|
83
|
+
|
|
84
|
+
The shared HTTP transport previously read up to65,536 bytes even for ping,
|
|
85
|
+
turning a valid200 acknowledgement with a larger ignored body into a delivery
|
|
86
|
+
failure. Node's notification method checks status without consuming the body.
|
|
87
|
+
The gem now requests headers-only handling for ping and closes its Net::HTTP
|
|
88
|
+
connection through the normal start/ensure cleanup. It does not follow redirects
|
|
89
|
+
or disable certificate/address validation. Key and sector fetches still read and
|
|
90
|
+
bound their bodies.
|
|
91
|
+
|
|
92
|
+
The real TCP test sends200 headers declaring10MB and never sends a body, then
|
|
93
|
+
observes peer closure; this proves the Ruby path does not implicitly drain.
|
|
94
|
+
Both installed gem and pinned Node TLS fixtures now send140,000 bytes on200 and
|
|
95
|
+
pass. [Local12 tests /113 assertions](../validation/ping-response-body-current.txt),
|
|
96
|
+
[installed artifact](../validation/ping-response-body-installed.txt),
|
|
97
|
+
[reference TLS](../validation/node-ping-response-body.txt).
|
|
98
|
+
The artifact also reruns oversized sector/JWKS rejection and untrusted TLS
|
|
99
|
+
scenarios, so the ping exception does not remove their protections.
|
|
100
|
+
|
|
101
|
+
## Initial management-update comparison (superseded below)
|
|
102
|
+
|
|
103
|
+
The gem snapshots the endpoint at request acceptance and checks current client
|
|
104
|
+
metadata before each dispatch. A deterministic barrier test now performs actual
|
|
105
|
+
registration-management PUT requests while completion's sender is paused. Both
|
|
106
|
+
endpoint replacement and switching to poll preserve the already approved result;
|
|
107
|
+
a subsequent retry raises Ineligible without sending, token collection succeeds
|
|
108
|
+
once, and replay is rejected. The in-flight sender retains only the original
|
|
109
|
+
endpoint and notification credential and runs outside a database transaction.
|
|
110
|
+
This is not atomic cancellation of a network operation.
|
|
111
|
+
|
|
112
|
+
Source comparison: provider9.12.2's `Client.backchannelPing` in
|
|
113
|
+
`lib/models/client.js` takes its destination from the Client instance on which it
|
|
114
|
+
is invoked, and the credential from the saved authentication request. It has no
|
|
115
|
+
comparison with an acceptance-time endpoint in that method. Consequently the
|
|
116
|
+
gem's snapshot check is a deliberate stricter credential-routing policy, not a
|
|
117
|
+
claim of identical management-update semantics. The real HTTP management race is now measured below.
|
|
118
|
+
|
|
119
|
+
[Gem barrier test](../../test/ciba_ping_test.rb),
|
|
120
|
+
[local execution](../validation/ping-management-current.txt).
|
|
121
|
+
|
|
122
|
+
[Three-database execution](../validation/ping-management-matrix.txt): three tests,
|
|
123
|
+
42 assertions per database, no failures/errors/skips.
|
|
124
|
+
|
|
125
|
+
### Reference HTTP management race
|
|
126
|
+
|
|
127
|
+
[Execution](../validation/node-ping-management.txt) uses provider9.12.2 with
|
|
128
|
+
actual authenticated registration and management PUT, and the existing verified
|
|
129
|
+
TLS receiver. A promise barrier pauses completion at the configured fetch
|
|
130
|
+
transport before the network request. The management update completes before
|
|
131
|
+
the barrier is released; no timing sleeps determine ordering.
|
|
132
|
+
|
|
133
|
+
| Update while completion is dispatching | Reference result | Gem result |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| Replace notification endpoint | In-flight send uses original endpoint; retry on a freshly loaded Client sends the saved bearer credential and request ID to the replacement endpoint | In-flight send uses original endpoint; subsequent retry raises Ineligible |
|
|
136
|
+
| Switch ping to poll | In-flight send uses original endpoint; retry on fresh Client raises TypeError without sending | In-flight send uses original endpoint; subsequent retry raises Ineligible without sending |
|
|
137
|
+
| Collect completed result after either update | First token request succeeds; repeated request returns invalid_grant | Same |
|
|
138
|
+
|
|
139
|
+
This establishes a concrete remaining policy difference, not failed CIBA token
|
|
140
|
+
issuance or unknown behavior. Matching the reference on endpoint replacement
|
|
141
|
+
would require changing the gem's acceptance-time destination binding and its
|
|
142
|
+
documented credential-routing guarantee. It has not been silently relaxed.
|
|
143
|
+
The current test does not establish cancellation, distributed-client cache
|
|
144
|
+
coherence, or every possible ordering of management and dispatch.
|
|
145
|
+
|
|
146
|
+
### Alignment correction: current registered destination
|
|
147
|
+
|
|
148
|
+
The earlier snapshot restriction was an extra gem policy, not a requirement
|
|
149
|
+
established by the reference. To follow the requested reference design,
|
|
150
|
+
`retry_ciba_ping` now reads the current eligible client's registered endpoint
|
|
151
|
+
when constructing each outgoing request. The saved endpoint column is retained
|
|
152
|
+
for schema compatibility but no longer controls routing. This replaces the
|
|
153
|
+
gem outcomes in the endpoint-replacement row above: subsequent retries now
|
|
154
|
+
reach the replacement endpoint with the saved credential, as the reference does.
|
|
155
|
+
|
|
156
|
+
Mode changes to poll still reject retries. Existing sends retain their selected
|
|
157
|
+
endpoint; result persistence and single token collection are unchanged. HTTPS
|
|
158
|
+
metadata validation, DNS/IP filtering, verified TLS and redirect rejection remain
|
|
159
|
+
active. Registration-management authority includes authority to receive pending
|
|
160
|
+
notification credentials at a replacement endpoint, matching the reference.
|
|
161
|
+
|
|
162
|
+
The updated tests first reproduced the old Ineligible failures
|
|
163
|
+
([red](../validation/ping-current-endpoint-red.txt)).
|
|
164
|
+
All ping tests then passed on three databases
|
|
165
|
+
([execution](../validation/ping-current-endpoint-matrix.txt)).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# RAR in the CIBA reference OP
|
|
2
|
+
|
|
3
|
+
Implementation update: the gem now has [optional RAR integration](../authorization-details.md), including registered/client type checks, saved consent, mandatory validation/issuance policy, output validation, JWT/token-response/opaque-introspection representation and ID Token exclusion. Tests now also race different narrowing requests and issuance/revocation on distinct DB connections. The earlier foundation and design notes below describe the implementation sequence; completion concurrency and additional policy/load-order review remain.
|
|
4
|
+
|
|
5
|
+
2026-09-30. Tested the installed, released node-oidc-provider 9.12.2 with a complete HTTP OP on Node24.21.0. RAR is enabled explicitly with a test-only `support_data` type, registered client permission for that type, and explicit application policy callbacks. The resource server uses opaque tokens for the introspection case. This is not a gem feature announcement.
|
|
6
|
+
|
|
7
|
+
## Observed behavior
|
|
8
|
+
|
|
9
|
+
| Scenario | Reference result |
|
|
10
|
+
|---|---|
|
|
11
|
+
| A client without the requested authorization-details type | `invalid_authorization_details` |
|
|
12
|
+
| A valid RAR request without a resource or configured default | `invalid_target` |
|
|
13
|
+
| Initial request with read/delete actions | Original JSON is stored in `BackchannelAuthenticationRequest.params`; source `rar` is absent |
|
|
14
|
+
| A saved Grant contains read-only `rar`, but completion omits `rar` | Token response has no authorization_details; neither the grant-source nor access-token policy callback ran in this case |
|
|
15
|
+
| Completion explicitly supplies read-only `rar` | Source carries that permission; access-token policy runs |
|
|
16
|
+
| Token request tries to add delete | The test application's policy returns only completed-source details, so the token response and opaque introspection contain only read |
|
|
17
|
+
| ID Token in the successful RAR flow | No authorization_details claim |
|
|
18
|
+
| Non-array JSON, unknown type, actions as a string | `invalid_authorization_details` |
|
|
19
|
+
|
|
20
|
+
The token-time narrowing result depends on the configured application callback. It is not evidence that the library automatically compares arbitrary JSON, intersects `Grant.rar`, or enforces all type-specific constraints. The fixture deliberately implements a simple policy that ignores token-time broadening and returns the previously completed source permission.
|
|
21
|
+
|
|
22
|
+
Evidence: [executable complete-OP harness](../../test/reference/full-op/lifecycle.mjs), [execution log](../validation/node-full-op-lts.txt). Earlier lifecycle, claims and resource cases still pass with RAR configured.
|
|
23
|
+
|
|
24
|
+
## Where the boundaries live
|
|
25
|
+
|
|
26
|
+
- `lib/shared/check_rar.js` parses and normalizes the request, checks registered types and client permission, then invokes the type's validator.
|
|
27
|
+
- `lib/shared/check_resource.js` requires a supplied/defaulted resource for these RAR requests. This is observed provider policy; do not label it a universal CIBA requirement.
|
|
28
|
+
- `lib/actions/authorization/ciba.js` retains request parameters and creates the asynchronous source. It does not run the authorization-code flow's grant-source RAR policy.
|
|
29
|
+
- `lib/provider.js#backchannelResult` accepts a separate `rar` completion option and stores it on the request. `Grant.addRar` alone does not populate it.
|
|
30
|
+
- `lib/helpers/grant_authorization.js#applyAuthorizationDetails` calls the configured access-token policy when token parameters or the completed source contain details.
|
|
31
|
+
- Introspection has its own policy callback. The fixture permits introspection only to the token's issuing client.
|
|
32
|
+
|
|
33
|
+
[RFC 9396](https://www.rfc-editor.org/rfc/rfc9396.html) defines authorization_details as an array of typed objects, describes token-request comparison and response/introspection representations, and leaves type semantics to the deployment. It does not provide a universal algorithm for narrowing arbitrary application data. Gem design must distinguish these protocol fields from the reference's CIBA completion API and application policy.
|
|
34
|
+
|
|
35
|
+
## Implementation progress: storage and structural validation only
|
|
36
|
+
|
|
37
|
+
The gem now contains `Schema.add_authorization_details`, an explicit additive helper for nullable request `requested_authorization_details`, saved-grant `authorization_details`, issued-token `ciba_authorization_details` and client `authorization_details_types` fields. The table names and request column can be overridden. It does not backfill consent, run automatically or enable RAR endpoints.
|
|
38
|
+
|
|
39
|
+
`Rodauth::CibaSupport::AuthorizationDetails.parse` is an internal structural validator (explicit require `rodauth/ciba/authorization_details`). It accepts bounded UTF-8 JSON arrays of typed objects and validates the common array/identifier fields. Limits are 8,192 bytes, 32 entries and JSON nesting depth 10. Returned arrays, hashes and strings are frozen recursively. Unknown type-specific fields are preserved; no authorization or narrowing semantics are inferred.
|
|
40
|
+
|
|
41
|
+
Duplicate keys are rejected at all depths using JSON's native `allow_duplicate_key: false`. This is a deliberate stricter input boundary than the reference's ordinary JSON.parse. A custom object-class experiment failed to detect duplicates consistently across JSON versions and was removed. JSON >=2.18,<3 is now an explicit runtime dependency; the lockfile selects 2.18.0. Invalid UTF-8 is checked on the byte sequence, including binary-encoded Ruby strings.
|
|
42
|
+
|
|
43
|
+
Tests in `test/ciba_authorization_details_test.rb` cover duplicate keys, malformed common fields, byte/count/depth limits, encoding, deep immutability and preservation of existing DB rows. This foundation alone does **not** implement request acceptance or authorization. The subsequent optional feature adds those integration paths with mandatory application policies; do not activate it based only on a successful migration.
|
|
44
|
+
|
|
45
|
+
## Integration requirements and original proposal
|
|
46
|
+
|
|
47
|
+
Use an opt-in capability and explicit additive migration. Preserve three distinct JSON values: original requested details on the CIBA request, approved details on the saved consent, and effective details on the issued OAuth token. Existing rows represent no additional permission. Do not infer approved details from the original request or mutate already-issued permissions when a saved grant changes.
|
|
48
|
+
|
|
49
|
+
Keep the existing saved-grant result API. At completion, bind the approved details to that request with application validation; at issuance, compile effective details from the stored request, saved consent, selected resource and token-time narrowing request. Any configurable policy must operate within that verified account/client/consent context. Fail closed if RAR is enabled without required validation/compilation policy. Do not implement a generic deep-merge or claim JSON equality means authorization.
|
|
50
|
+
|
|
51
|
+
Required validation includes bounded JSON, array/object/type shape, common field shapes, registered types and per-client allowed types. Type-specific account/operation/amount/target constraints remain application policy. Validate compiled output as well as input. Policy failures during completion or issuance retain the gem's accepted atomic rollback contract.
|
|
52
|
+
|
|
53
|
+
RAR output belongs in the access token/token response and opaque introspection, not the ID Token. Preserve the resource audience boundary and explicit-claims filtering. Add Discovery and static-client metadata only with implemented enforcement. Reject poll attempts to expand permission; a policy's narrower result must be testable with a concrete type.
|
|
54
|
+
|
|
55
|
+
Before claiming support: implement the schema/API and client metadata; test missing policy, disabled/mixed-version states, unrequested/unapproved details, request/result snapshot tampering, type/resource/client isolation, hook-time changes, issuance/revocation races, JWT representation and opaque introspection. Add installed-gem smoke coverage. Generic RAR support and the customer's particular support/deletion approval semantics are separate deliverables.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# CIBA refresh token: reference contract
|
|
2
|
+
|
|
3
|
+
2026-09-30。参照はインストール済み `oidc-provider 9.12.2`。これは仕様全体の適合試験ではなく、既定設定の実測と上流コードの比較である。gem の [refresh 基本実装](../refresh-tokens.md)は開発途中で、公開可能な対応完了ではない。
|
|
4
|
+
|
|
5
|
+
## HTTP で確認したこと
|
|
6
|
+
|
|
7
|
+
`test/reference/full-op/lifecycle.mjs` を Node 24.21.0、固定 Docker image、network none で実行した。[実行ログ](../validation/node-refresh-reference.txt)。既存 lifecycle ケースも成功。今回 ping.mjs は再実行していない。
|
|
8
|
+
|
|
9
|
+
- client が `refresh_token` grant を登録し、CIBA 要求が `offline_access` を含む場合のみ、既定 policy は refresh token を発行する。片方だけの場合は発行しない。
|
|
10
|
+
- 更新で元の scope にない `read` を要求すると `invalid_scope`。
|
|
11
|
+
- `scope=openid` に縮小した更新の次に scope を省略すると、元の `openid offline_access` に戻る。更新時の縮小はそのアクセストークンだけに適用される。
|
|
12
|
+
- 発行直後の confidential client の refresh token は、既定 rotation policy では同じ値で再利用できる。
|
|
13
|
+
- refresh token の保存モデルには元の authTime/acr/amr が残り、更新後 ID Token は元の nonce を持つ。この試験の claim 設定では auth_time/acr/amr は出力されない。保存と claim 出力の条件を混同しない。
|
|
14
|
+
- 保存 Grant を削除すると更新は `invalid_grant` になる。
|
|
15
|
+
- 保存モデルの iat/exp を変更し TTL の80%が経過した fixture では、既定 policy が rotation し、旧 token に consumed、後続 token に同じ grantId と rotations=1 を保存する。実時間で寿命の80%を待った試験ではない。
|
|
16
|
+
- 別の refresh 対応 client が旧 token を使用しても `invalid_grant` で拒否するだけで Grant は残る。正当な client が使用済み token を再利用すると Grant が消え、後続 refresh token は使用不能、後続 access token の introspection は active=false になる。
|
|
17
|
+
- exp を過去にした保存 fixture は `invalid_grant`。その Grant は保持される。memory adapter の期限管理も含むため、expired token が必ず handler の期限分岐まで到達した証拠ではない。
|
|
18
|
+
- resource A/B を要求して A の read token を最初に発行しても、refresh は B の write token を取得できる。resource 省略では OIDC scope に戻り、未要求の宛先は `invalid_target`。
|
|
19
|
+
- 保存 Grant の B/write を後から拒否すると B の refresh は成功するが scope は空になる。初回 token の scope を refresh の元許可として使ってはいけない。
|
|
20
|
+
- revocation の既定 policy は別 confidential client の live token 取消しを400 `invalid_request` で拒否する。正当な client の取消しは空bodyの200で、保存 Grant ごと無効化する。hint の省略・`refresh_token`・`access_token`・未知値で同じ結果。取消し後は別 client の再送も200になる。
|
|
21
|
+
- claims/RAR を伴う rotation は source の claims/rar/authTime を後続に保持する。保存 Grant で後から拒否した email は次の ID Token に含まれない。参照アプリの RAR policy は完了時に明示した source.rar を返し、更新パラメータで read/delete を再要求しても read のまま。RAR policy は固定したテストアプリのものであり、一般的な権限縮小アルゴリズムではない。
|
|
22
|
+
|
|
23
|
+
## コードから確認した境界
|
|
24
|
+
|
|
25
|
+
参照側 `lib/helpers/defaults.js` の issueRefreshToken/rotateRefreshToken、`lib/helpers/grant_common.js`、`lib/actions/grants/refresh_token.js` を確認した。
|
|
26
|
+
|
|
27
|
+
参照側は RefreshToken を AccessToken とは別のモデルとして保存し、元の scope、resource、claims、rar、nonce、認証情報、grantId を保持する。更新では保存 Grant を検証し、resource/scope/承認から新しいアクセストークンを構成する。rotation 後の使用済み token の再利用は関連 Grant の取消しへ進む。上記 HTTP ケースで rotation/replay/resource を確認したが、claims/RAR の refresh 併用は未検証。
|
|
28
|
+
|
|
29
|
+
rodauth-oauth 1.7.0 の `oauth_base.rb#create_token` は既定で毎回 rotation し、`create_token_from_token` は既存 oauth_grants 行の token と期限を更新する。元の許可を保持する独立した RefreshToken モデルではない。比較開始時の gem は refresh grant を super に渡していた。現在の開発実装は CIBA 専用 namespace の token を別処理し、それ以外を super に渡す。
|
|
30
|
+
|
|
31
|
+
したがって `generate_token(..., true)` への変更だけでは整合しない。CIBA 元要求の許可と、今回の access token に限定された scope/resource/claims/RAR を分け、保存承認の期限・取消し・account/client を更新でも検証する必要がある。通常の OAuth refresh の既定設定を OP 全体で変更しない。
|
|
32
|
+
|
|
33
|
+
## 次の実装前に残る検証
|
|
34
|
+
|
|
35
|
+
- refresh での claims/RAR、rotation 後の認証情報・元許可の保持。
|
|
36
|
+
- gem の更新処理、同時更新・取消し、発行失敗 rollback、機能無効化。保存 migration のみ下記の段階まで実装。
|
|
37
|
+
|
|
38
|
+
上記のうち更新・rotation/replay・署名失敗 rollback・無効化・同時 rotation は基本実装へ接続した。refresh token を提示する取消し endpoint、期限切れ cleanup、更新 hooks/events も接続した。claims/RAR の opaque/JWT 更新は元の要求・現在の承認・アプリ policy の分離を試験した。access token を提示する grant-wide 取消し、Ruby matrix・配布 gem での検証は残る。
|
|
39
|
+
|
|
40
|
+
## 保存基盤の実装段階
|
|
41
|
+
|
|
42
|
+
`Schema.create_refresh_tokens(db, ...)` を追加した。明示的な追加 migration であり、呼び出しても refresh の受付・発行は有効にならない。まだ導入用の機能として公開する段階ではない。
|
|
43
|
+
|
|
44
|
+
- `token_digest` の一意制約と `consumed_at` によって、rotation 後も旧 token の使用済み記録を保持できる。生の refresh token 用の列は持たない。
|
|
45
|
+
- 元の account/client、保存承認への外部キー、scope、要求した claims/resources/RAR、認証時刻・acr/amr・nonce を、access token の絞り込まれた属性と別に保存する。
|
|
46
|
+
- 初回作成時刻・今回発行時刻・期限・rotation 回数を持ち、node の経過時間による rotation を実装するための情報を保持する。lock_version は既存の CIBA と同様に DB の競合制御に使用する予定。
|
|
47
|
+
- 要求テーブルへの外部キーを持たず、短寿命の要求を cleanup しても refresh source は残る。保存承認・account/client の削除時は cascade する。承認の論理取消しは更新処理で検証する必要があり、この migration だけでは強制されない。
|
|
48
|
+
|
|
49
|
+
保存テストは既存要求・発行 token・承認の保持、digest 重複拒否、使用済みと後続 token の共存、要求削除後の保持、承認削除時の cascade、transaction rollback、テーブル名変更を対象とする。この段階の後、基本プロトコル処理と replay 取消しを実装した。
|
|
50
|
+
|
|
51
|
+
Ruby 4.0.6 の SQLite/PostgreSQL/MySQL で各 **2 tests / 17 assertions** 成功。[限定 matrix のログ](../validation/refresh-storage-matrix.txt)。これは追加 storage テストのみの実行であり、既存全 suite の再実行や Ruby 3.3/3.4 の確認ではない。
|
|
52
|
+
|
|
53
|
+
基本 runtime 接続後、同じ3 DBで全 suite 各 **190 tests** 成功(SQLite 1709 assertions、PostgreSQL/MySQL 1706)。[runtime matrix](../validation/refresh-runtime-matrix.txt)。同時 rotation、通常 OAuth refresh の維持、client 登録削除、認証情報・scope 保持、resource の宛先変更を含む。auth_time は既存 completion API と同じく省略可能なので nullable に修正した。Ruby 3.3/3.4・配布成果物での refresh 検証はまだ行っていない。
|
|
54
|
+
|
|
55
|
+
refresh の revocation 接続後は3 DBで各192 tests成功(1756〜1759 assertions)。その後の取消し済み token の扱いの調整は、3 DB各2 tests/54 assertionsで確認した。[全体](../validation/refresh-revocation-matrix.txt)、[最終差分](../validation/refresh-revocation-final.txt)。上流の browser session 分岐をこの名前空間の取消しだけで使わず、client 認証に固定した。通常 OAuth token の取消し経路は変更していない。
|
|
56
|
+
|
|
57
|
+
claims/RAR の opaque/JWT rotation と cleanup 追加後は3 DB各 **195 tests** 成功(1831〜1834 assertions)。[ログ](../validation/refresh-permissions-matrix.txt)。未承認操作の追加拒否、rotation rollback、現在の claim 拒否、後続 source の保持、RAR 無効化、許可縮小、期限境界・外側 rollback を含む。汎用 RAR 意味論や全 callback の組合せを網羅するものではない。
|
|
58
|
+
|
|
59
|
+
更新フック追加後は3 DBと Ruby3.3/3.4 SQLite の5環境で各 **200 tests** 成功(1894〜1897 assertions)。[DB](../validation/refresh-hooks-matrix.txt)、[Ruby](../validation/refresh-ruby-matrix.txt)。フック後の再検証、rollback、同一 source の再入拒否、outer commit 後の観測と失敗の分離を確認。残件のうち Ruby matrix は解消したが、配布 gem での refresh と access-token 取消しの整合化は未完了。
|
|
60
|
+
|
|
61
|
+
その後、配布 gem の opaque/JWT refresh smoke を追加して成功した。[ログ](../validation/refresh-package.txt)。CIBA の signed request・claims/resource/RAR と組み合わせ、rotation・認証情報保持・取消し・cleanup を検証した。Discovery の revocation URL 欠落も修正した。配布検証の残件は解消したが、access-token を提示した grant-wide 取消しの整合化は残る。
|
|
62
|
+
|
|
63
|
+
## JWT client assertion audience
|
|
64
|
+
|
|
65
|
+
A separate complete-OP fixture now exercises both `private_key_jwt` (RS256) and
|
|
66
|
+
`client_secret_jwt` (HS256), with a fresh signed client assertion at each CIBA
|
|
67
|
+
initiation, token collection and refresh. Node 9.12.2 accepts the issuer as aud
|
|
68
|
+
at all three stages. Wrong audiences and reuse of the same assertion fail client
|
|
69
|
+
authentication. [Reference run](../validation/node-assertion-refresh.txt),
|
|
70
|
+
[harness](../../test/reference/full-op/assertion-refresh.mjs).
|
|
71
|
+
|
|
72
|
+
The gem originally accepted issuer aud for initiation/collection but rejected
|
|
73
|
+
it for CIBA refresh with HTTP401. Both authentication methods reproduced the
|
|
74
|
+
failure. The corrected audience rule includes the issuer for CIBA refresh only,
|
|
75
|
+
selected by the existing reserved `ciba_rt_` namespace; it does not grant access
|
|
76
|
+
without client signature/secret, replay checks and refresh ownership/consent
|
|
77
|
+
validation. Unrelated authorization-code requests retain the previous audience
|
|
78
|
+
rule. Wrong-audience attempts preserve refresh rows, and assertion replay after
|
|
79
|
+
one successful refresh cannot issue another token.
|
|
80
|
+
[Before correction](../validation/assertion-refresh-red.txt),
|
|
81
|
+
[local regressions](../validation/assertion-refresh-current.txt).
|
|
82
|
+
|
|
83
|
+
This proves these two authentication methods with issuer aud and the fixture's
|
|
84
|
+
algorithms. It is not mTLS support or coverage of every authentication method,
|
|
85
|
+
JWT algorithm, key rotation or remote-key refresh failure.
|