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,423 @@
|
|
|
1
|
+
# CIBA DPoP integration: measured contract and open implementation
|
|
2
|
+
|
|
3
|
+
The complete node-oidc-provider 9.12.2 runs over loopback HTTP with a confidential
|
|
4
|
+
Basic client, `dpop_bound_access_tokens: true`, ES256 proof keys, poll delivery,
|
|
5
|
+
offline_access and refresh. The initial cases disable nonce challenges; later
|
|
6
|
+
providers exercise required and optional nonce policies. The
|
|
7
|
+
built-in memory adapter is used; no protocol or cryptographic code is stubbed.
|
|
8
|
+
[Harness](../../test/reference/full-op/dpop.mjs),
|
|
9
|
+
[initial static-client run](../validation/node-dpop-reference.txt).
|
|
10
|
+
The harness now creates its proof-required client through authenticated dynamic
|
|
11
|
+
registration and also exercises management replacement.
|
|
12
|
+
[Current registration run](../validation/node-dpop-registration.txt).
|
|
13
|
+
|
|
14
|
+
## Measured reference behavior
|
|
15
|
+
|
|
16
|
+
- Initiation needs client authentication but no DPoP proof. Poll without proof
|
|
17
|
+
returns invalid_grant for this client. A valid proof before approval returns
|
|
18
|
+
authorization_pending.
|
|
19
|
+
- A random nonempty jti is accepted. Wrong method, wrong target URI, old iat and
|
|
20
|
+
empty jti produce invalid_dpop_proof without consuming the approved request.
|
|
21
|
+
- Successful issuance returns token_type DPoP and stores the proof's JWK
|
|
22
|
+
thumbprint in the access token's jkt.
|
|
23
|
+
- UserInfo rejects bearer presentation of the bound token, another proof key,
|
|
24
|
+
and incorrect ath. Matching key and access-token hash succeeds. Reusing the
|
|
25
|
+
same proof jti is rejected with HTTP401.
|
|
26
|
+
- This confidential client's refresh token has no jkt. Refresh still needs a
|
|
27
|
+
proof due to the client's policy, but a new key is accepted and binds the new
|
|
28
|
+
access token. Reusing exactly the same refresh proof is rejected.
|
|
29
|
+
|
|
30
|
+
The distinction between access-token binding and confidential refresh-token
|
|
31
|
+
authentication is intentional in the measured code (`set_rt_bindings.js`). Do
|
|
32
|
+
not impose a fixed refresh key based solely on the access token's jkt. Public
|
|
33
|
+
clients are not part of this CIBA fixture. Nonces, DCR, JWT resource access
|
|
34
|
+
tokens, revocation, key changes during issuance, all URI-normalization cases,
|
|
35
|
+
and concurrent proof replays are not established by this run.
|
|
36
|
+
|
|
37
|
+
## Baseline Ruby integration gaps
|
|
38
|
+
|
|
39
|
+
Enabling upstream `oauth_dpop` alone is not CIBA DPoP support. A separate
|
|
40
|
+
characterization probe enables it in the actual gem Rack stack with the
|
|
41
|
+
upstream proof table and token thumbprint column:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
mise exec ruby -- ruby test/ciba_dpop_test.rb
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The probe confirms that a cryptographically valid random-jti proof is rejected
|
|
48
|
+
with invalid_dpop_proof / Invalid DPoP jti. The upstream claim validator requires
|
|
49
|
+
the SHA256 hex digest of method, full URL and iat instead of an arbitrary unique
|
|
50
|
+
identifier. With that custom identifier, a one-hour-old proof issues an access
|
|
51
|
+
token; the identical proof also issues a second independently approved request.
|
|
52
|
+
The upstream first-use table does not reject reuse within its configured window.
|
|
53
|
+
[Evidence](../validation/upstream-dpop-probe.txt): 1 characterization test,
|
|
54
|
+
13 assertions. That historical characterization probe has now been replaced
|
|
55
|
+
by the normal regression tests above; it is no longer an executable expectation
|
|
56
|
+
of defective behavior.
|
|
57
|
+
|
|
58
|
+
These observations do not show bypass of client authentication or customer
|
|
59
|
+
approval: both were valid in each request. They show why proof freshness and
|
|
60
|
+
replay guarantees must not be inferred from the advertised upstream feature.
|
|
61
|
+
The initial production correction is described below; this historical log does
|
|
62
|
+
not describe the corrected token endpoint.
|
|
63
|
+
|
|
64
|
+
## Initial CIBA token-endpoint correction
|
|
65
|
+
|
|
66
|
+
When upstream `oauth_dpop` is enabled, a prepended CIBA boundary now validates
|
|
67
|
+
proofs for the CIBA grant and this gem's refresh tokens. Other OAuth grant types
|
|
68
|
+
retain upstream behavior. It accepts nonempty random jti values, verifies the
|
|
69
|
+
embedded public key/signature and the configured asymmetric algorithm, rejects
|
|
70
|
+
critical extensions and private key material, and checks method, token URI and
|
|
71
|
+
iat within 300 seconds in either direction. Query/fragment are removed from the
|
|
72
|
+
proof URI before comparison. Limits are 8192 proof bytes and 1024 jti bytes.
|
|
73
|
+
|
|
74
|
+
Proof replay is claimed only when issuing a token, in the same DB transaction
|
|
75
|
+
as request consumption and token insertion. The existing explicit
|
|
76
|
+
`Schema.create_client_assertions` ledger is reused with a distinct `ciba_dpop`
|
|
77
|
+
digest namespace and client ID; no raw proof or jti is stored. Expiry extends
|
|
78
|
+
beyond the last accepted proof instant, so `cleanup_ciba_client_assertions`
|
|
79
|
+
cannot reopen replay within the validation window. The upstream token table
|
|
80
|
+
must have its configured `oauth_grants_dpop_jkt_column`; missing storage prevents
|
|
81
|
+
startup when the upstream feature is enabled. Ordinary upstream DPoP flows still
|
|
82
|
+
need their own upstream proof-table setup.
|
|
83
|
+
|
|
84
|
+
Pending polls do not reserve a proof. Simultaneous use across two approved
|
|
85
|
+
requests produces only one issuance; the losing request remains approved.
|
|
86
|
+
Signing failures roll back both consumption and replay insertion, permitting a
|
|
87
|
+
retry. This follows the accepted gem atomicity policy instead of node's
|
|
88
|
+
nontransactional failure consumption. A proof-required client without a proof
|
|
89
|
+
gets invalid_grant. Required proofs no longer implicitly enable nonce challenges;
|
|
90
|
+
`oauth_dpop_use_nonce` remains an explicit option.
|
|
91
|
+
|
|
92
|
+
Opaque and JWT confidential refresh accepts a new proof key and binds the new
|
|
93
|
+
access token to it. Reusing that proof fails without changing refresh state.
|
|
94
|
+
Seven local tests pass (90 assertions), including failure rollback, replay
|
|
95
|
+
concurrency, malformed/old proofs, URI handling and preservation of ordinary
|
|
96
|
+
grant validation. [Local log](../validation/ciba-dpop-current.txt).
|
|
97
|
+
The full three-DB suite passed 273 tests before the last malformed JOSE/signature
|
|
98
|
+
assertions; all seven final DPoP tests (90 assertions) then passed on each DB.
|
|
99
|
+
[Full run](../validation/ciba-dpop-matrix.txt), [final cases](../validation/ciba-dpop-final.txt).
|
|
100
|
+
|
|
101
|
+
This is **not full DPoP support**. External resource integration,
|
|
102
|
+
cleanup/retained-key combinations,
|
|
103
|
+
broader algorithm and URI normalization equivalence, and deployment tests still
|
|
104
|
+
need integration. The nonce extension below supersedes the unconditional
|
|
105
|
+
300-second iat restriction. Do not advertise a completed DPoP feature on this basis.
|
|
106
|
+
|
|
107
|
+
## CIBA UserInfo binding and replay
|
|
108
|
+
|
|
109
|
+
Upstream's UserInfo route calls `authorization_token` directly, bypassing
|
|
110
|
+
`require_oauth_authorization`. The extension therefore applies its proof checks
|
|
111
|
+
at that actual entry point for recognized CIBA credentials. Namespaced/legacy
|
|
112
|
+
opaque tokens and marked JWTs select this path; unverified JWT metadata is used
|
|
113
|
+
only to select stricter validation, never to authorize access. Signature,
|
|
114
|
+
expiry, client/subject provenance and a stored grant are still required.
|
|
115
|
+
|
|
116
|
+
A DPoP presentation must have a fresh, signed proof with the UserInfo method and
|
|
117
|
+
URI, matching ath, and a key thumbprint matching both the access-token claims
|
|
118
|
+
and the saved issuance row. Replay is claimed only after token and key checks.
|
|
119
|
+
Unknown tokens, wrong keys/hashes, forged JWTs, expired/revoked rows and bearer
|
|
120
|
+
presentation of bound tokens return HTTP401. Unbound CIBA Bearer Tokens continue
|
|
121
|
+
to work. Successful UserInfo proof claims commit before account-claim rendering;
|
|
122
|
+
a later application rendering failure requires a fresh proof on retry.
|
|
123
|
+
|
|
124
|
+
The upstream DPoP datasets append an OR for matching keys to the whole query.
|
|
125
|
+
That can admit revoked/expired rows, or rows not matching the presented token.
|
|
126
|
+
The CIBA UserInfo path rebuilds the predicates conjunctively: identity/token,
|
|
127
|
+
type, expiration, revocation and key binding must all match. Both the initial
|
|
128
|
+
lookup and UserInfo's later grant lookup retain those constraints. Other OAuth
|
|
129
|
+
flows are delegated unchanged; this is not a global upstream security repair.
|
|
130
|
+
|
|
131
|
+
The opaque/JWT tests verify concurrent proof reuse has one success and one401,
|
|
132
|
+
negative requests do not claim proofs, a known key cannot authorize an unknown
|
|
133
|
+
token, and expiration/revocation remain effective. Local DPoP tests pass
|
|
134
|
+
**9 tests / 135 assertions**. [Log](../validation/ciba-dpop-userinfo-current.txt).
|
|
135
|
+
Full Ruby4.0.6 suites pass **275 tests** on SQLite, PostgreSQL and MySQL
|
|
136
|
+
(SQLite skips two row-lock-specific tests).
|
|
137
|
+
[Three-DB log](../validation/ciba-dpop-userinfo-matrix.txt).
|
|
138
|
+
The original failure is retained in [this log](../validation/ciba-dpop-userinfo-red.txt).
|
|
139
|
+
The later POST/storage checks below extend this evidence. Pairwise combinations
|
|
140
|
+
and external resource-server enforcement remain to be exercised for this addition.
|
|
141
|
+
|
|
142
|
+
## Dynamic registration of proof-required clients
|
|
143
|
+
|
|
144
|
+
With `oauth_dpop` enabled, CIBA registration accepts only JSON booleans for
|
|
145
|
+
`dpop_bound_access_tokens`. Strings such as "true"/"false", numeric values,
|
|
146
|
+
arrays and objects are rejected before insertion. Supplying the property requires
|
|
147
|
+
a Boolean client column at `oauth_applications_dpop_bound_access_tokens_column`
|
|
148
|
+
(default `dpop_bound_access_tokens`); an absent storage column causes rejection.
|
|
149
|
+
Omission means false. When that column exists, the default is persisted as false;
|
|
150
|
+
when it is absent and the property is omitted, the implicit false needs no storage.
|
|
151
|
+
|
|
152
|
+
The returned registration credentials can initiate CIBA normally, but the token
|
|
153
|
+
endpoint rejects collection without a proof if the registered flag is true.
|
|
154
|
+
Valid proofs issue bound tokens. Invalid management PUT preserves both policy
|
|
155
|
+
and management credentials. A full replacement that omits the flag resets it to
|
|
156
|
+
false, including the management response; false must not disappear because of
|
|
157
|
+
upstream's truthy-only serializer. New token issuance can then omit the proof,
|
|
158
|
+
but already-issued bound tokens still require it at UserInfo.
|
|
159
|
+
|
|
160
|
+
With the upstream DPoP feature disabled, this metadata is ignored and is not
|
|
161
|
+
stored or echoed. Both reference and gem fixtures check this explicitly, even
|
|
162
|
+
when a similarly named DB column exists in the gem application.
|
|
163
|
+
|
|
164
|
+
The node harness performs DCR, CIBA issuance, UserInfo, confidential refresh and
|
|
165
|
+
management with the issued client. Gem tests additionally check missing storage,
|
|
166
|
+
no insertion for invalid values, whole-row preservation on failed updates and
|
|
167
|
+
retention of the old token's binding. [Reference](../validation/node-dpop-registration.txt),
|
|
168
|
+
[local integration](../validation/dpop-registration-current.txt).
|
|
169
|
+
The registration/management/DPoP subset passes **44 tests** on each of SQLite,
|
|
170
|
+
PostgreSQL and MySQL, without failures/errors/skips.
|
|
171
|
+
[DB log](../validation/dpop-registration-matrix.txt). This is targeted evidence,
|
|
172
|
+
not a rerun of the complete Ruby/version/package matrix.
|
|
173
|
+
|
|
174
|
+
## Server nonce policy and replay retention
|
|
175
|
+
|
|
176
|
+
The CIBA boundary now follows the reference nonce policy: a missing required
|
|
177
|
+
nonce produces `use_dpop_nonce` with `DPoP-Nonce` (HTTP400 at token, HTTP401 at
|
|
178
|
+
UserInfo). A valid nonce replaces proof iat freshness, but never replaces
|
|
179
|
+
signature, method, URI, key, token, client or approval checks. Non-string nonce
|
|
180
|
+
claims are invalid proofs; an incorrect or expired string nonce gets a fresh
|
|
181
|
+
challenge. One server nonce can be used across these endpoints with distinct
|
|
182
|
+
proofs. Reusing a proof still fails. With nonce support enabled but not required,
|
|
183
|
+
a fresh proof may omit it; an old proof gets a challenge.
|
|
184
|
+
|
|
185
|
+
Enable upstream `oauth_dpop` and configure `ciba_dpop_nonce_secret` with an
|
|
186
|
+
independent random **32-byte String**, persisted and shared by all processes of
|
|
187
|
+
the issuer. Keep different issuers' secrets separate. Set `oauth_dpop_use_nonce`
|
|
188
|
+
to true to require nonces; its default remains false. A missing secret with
|
|
189
|
+
required nonces, or a supplied secret of the wrong type/length, prevents startup.
|
|
190
|
+
Do not regenerate the secret per request or process start. Rotation invalidates
|
|
191
|
+
old challenges; clients obtain a fresh one and retry. No raw secret is stored in
|
|
192
|
+
the replay ledger.
|
|
193
|
+
|
|
194
|
+
The construction follows reference `helpers/challenge.js`: HKDF-SHA256 with
|
|
195
|
+
`DPoP` info, a big-endian 64-bit minute counter as salt, and a 32-byte base64url
|
|
196
|
+
result. The accepted counters are current minus two through current plus two;
|
|
197
|
+
the issued counter is current plus one. This is a moving five-step window, not
|
|
198
|
+
a fixed five-minute lifetime after receipt. It supersedes upstream's
|
|
199
|
+
`oauth_dpop_nonce_expires_in` setting for the CIBA boundary only.
|
|
200
|
+
|
|
201
|
+
Nonce-validated proof claims remain in the shared DB replay ledger until at
|
|
202
|
+
least validation time plus 301 seconds. Using an ancient proof iat for this
|
|
203
|
+
expiry would let cleanup reopen replay while the nonce remains valid. Opaque
|
|
204
|
+
and JWT integration tests exercise cleanup followed by replay, token/UserInfo
|
|
205
|
+
challenge and retry, expiry and secret rotation. Without a verified nonce, the
|
|
206
|
+
ledger still retains the proof beyond its accepted iat window.
|
|
207
|
+
|
|
208
|
+
[Real OP nonce run](../validation/node-dpop-nonce.txt),
|
|
209
|
+
[local gem run](../validation/dpop-nonce-current.txt): **13 tests / 218 assertions**.
|
|
210
|
+
Both implementations also reject zero iat even with a valid nonce; the added
|
|
211
|
+
regression originally failed for both opaque and JWT gem issuance.
|
|
212
|
+
[Failure before correction](../validation/dpop-nonce-zero-iat-red.txt).
|
|
213
|
+
The local cases include both opaque and JWT access tokens; the reference uses
|
|
214
|
+
opaque access tokens. Multi-process deployment, clock-skew boundaries and
|
|
215
|
+
installed-package validation are not established by these runs.
|
|
216
|
+
|
|
217
|
+
## Installed-artifact integration
|
|
218
|
+
|
|
219
|
+
`bin/verify-package` now builds the gem, installs it into a temporary directory,
|
|
220
|
+
and runs the packaged `examples/dpop_smoke.rb` without repository load paths.
|
|
221
|
+
It explicitly checks that both the CIBA feature and DPoP module were loaded from
|
|
222
|
+
the installed gem. No test helpers or stubbed proof validators are used.
|
|
223
|
+
|
|
224
|
+
For opaque and JWT access tokens, the example dynamically registers a
|
|
225
|
+
proof-required confidential client, initiates and approves CIBA, receives a nonce
|
|
226
|
+
challenge and issues tokens with an old-iat proof plus valid nonce. It verifies
|
|
227
|
+
the ID Token signature/issuer/audience, JWT access-token signature and cnf when
|
|
228
|
+
applicable, and stored key binding. Cleanup followed by proof replay cannot
|
|
229
|
+
consume a second approval. UserInfo rejects bearer presentation, a wrong ath,
|
|
230
|
+
another proof key and replay, while the correctly bound proof returns the
|
|
231
|
+
customer subject. Refresh accepts a new proof key, returns a token bound to it,
|
|
232
|
+
and that token accesses UserInfo with the same customer subject. Reusing the
|
|
233
|
+
refresh proof fails.
|
|
234
|
+
|
|
235
|
+
[Package run](../validation/dpop-installed-package.txt). This uses Ruby4.0.6,
|
|
236
|
+
SQLite and Rack requests in-process, along with the existing independent HTTPS
|
|
237
|
+
ping/JWKS/sector-document fixtures. It does not prove DPoP reverse-proxy/header
|
|
238
|
+
handling, external resource-server enforcement or multi-process behavior.
|
|
239
|
+
Dependencies are reused locally; fresh network dependency resolution and
|
|
240
|
+
publication are not part of this verification.
|
|
241
|
+
|
|
242
|
+
## POST UserInfo and token storage
|
|
243
|
+
|
|
244
|
+
The reference OP accepts POST UserInfo with a POST-bound proof, rejects a
|
|
245
|
+
GET-bound proof on that route and rejects replay of the accepted proof.
|
|
246
|
+
[Reference run](../validation/node-dpop-post.txt).
|
|
247
|
+
The gem exercises POST for both opaque and JWT tokens with the same checks as
|
|
248
|
+
GET: unbound bearer success, bound bearer rejection, key/ath/method validation,
|
|
249
|
+
subject matching, concurrent proof exclusion, unknown/forged credentials and
|
|
250
|
+
expired/revoked token rejection. These cases pass without a production change.
|
|
251
|
+
|
|
252
|
+
Correction to the earlier coverage assessment: upstream defaults
|
|
253
|
+
`oauth_grants_token_hash_column` to `:token`. The original gem fixtures already
|
|
254
|
+
used hashed opaque-token storage, despite the column name; they did not test
|
|
255
|
+
only plaintext storage. Tests now explicitly assert the stored hash and add
|
|
256
|
+
POST cases for a custom `:token_digest` column and for the opt-in plaintext
|
|
257
|
+
configuration (`oauth_grants_token_hash_column nil`). The custom-column case
|
|
258
|
+
also asserts that the ordinary `:token` column remains empty. No storage default
|
|
259
|
+
has changed. [Local run](../validation/dpop-post-storage-current.txt):
|
|
260
|
+
**17 tests / 321 assertions**.
|
|
261
|
+
Including DPoP registration cases, all **19 tests / 370 assertions** pass on
|
|
262
|
+
SQLite, PostgreSQL and MySQL with no failures/errors/skips.
|
|
263
|
+
[DB run](../validation/dpop-post-storage-matrix.txt).
|
|
264
|
+
|
|
265
|
+
These Rack tests use Authorization headers. They do not establish support for
|
|
266
|
+
body/query token presentation or every proxy/header transformation.
|
|
267
|
+
|
|
268
|
+
## Opaque-token introspection binding
|
|
269
|
+
|
|
270
|
+
The real reference OP returns `active: true`, `token_type: DPoP` and `cnf.jkt`
|
|
271
|
+
for an active opaque bound token without a DPoP proof on introspection. The
|
|
272
|
+
caller authenticates as an introspection client; this is distinct from presenting
|
|
273
|
+
the access token to a resource server. A refreshed token reports the new proof
|
|
274
|
+
key. Unknown and revoked tokens return only `active: false`.
|
|
275
|
+
[Reference](../validation/node-dpop-introspection.txt).
|
|
276
|
+
|
|
277
|
+
The gem initially returned inactive for these valid tokens: upstream's grant
|
|
278
|
+
lookup filtered for a nil DPoP key when no proof was present. Also, upstream's
|
|
279
|
+
introspection serializer read cnf from JWT claims, not the opaque issuance row.
|
|
280
|
+
The CIBA-only correction looks up recognized opaque CIBA tokens by their exact
|
|
281
|
+
stored token/hash, grant type, expiry and revocation, and returns the saved jkt
|
|
282
|
+
with token type DPoP. It retains existing client authentication. It does not
|
|
283
|
+
change ordinary OAuth grant lookup or authorize resource access.
|
|
284
|
+
|
|
285
|
+
Regression tests cover both ordinary OIDC and resource-targeted CIBA opaque
|
|
286
|
+
tokens, cnf/type, resource audience/scope, wrong client credentials, unknown,
|
|
287
|
+
expired and revoked tokens, and unchanged handling of an unrelated bound OAuth
|
|
288
|
+
grant. Introspection does not claim a DPoP proof or touch the replay ledger.
|
|
289
|
+
[Original failure](../validation/dpop-introspection-red.txt),
|
|
290
|
+
[local tests](../validation/dpop-introspection-current.txt).
|
|
291
|
+
The full Ruby4.0.6 suite passes **287 tests** on each of SQLite, PostgreSQL and
|
|
292
|
+
MySQL; SQLite skips two row-lock-specific tests.
|
|
293
|
+
[Full DB matrix](../validation/dpop-introspection-matrix.txt).
|
|
294
|
+
|
|
295
|
+
Applications still own which authenticated introspection callers may receive
|
|
296
|
+
token information. Resource servers must themselves enforce cnf/proof matching,
|
|
297
|
+
signature, ath, HTTP method/URI, freshness, replay and their audience/scope policy.
|
|
298
|
+
This change supplies the OP-side binding information; it does not implement an
|
|
299
|
+
external resource server. Existing resource-JWT introspection restrictions remain.
|
|
300
|
+
|
|
301
|
+
The installed-package fixture initially retrieved the introspection URL from OAuth
|
|
302
|
+
authorization-server metadata and verified active/cnf/type for an opaque token.
|
|
303
|
+
[Package run](../validation/dpop-introspection-package.txt).
|
|
304
|
+
Upstream's OIDC metadata allowlist had removed that URL and DPoP algorithm
|
|
305
|
+
metadata. The correction below supersedes this earlier restriction.
|
|
306
|
+
|
|
307
|
+
## Discovery extension metadata
|
|
308
|
+
|
|
309
|
+
OIDC Discovery now retains introspection endpoint/auth metadata and
|
|
310
|
+
`dpop_signing_alg_values_supported` from the actual OAuth authorization-server
|
|
311
|
+
metadata. It does not hardcode a URL or a new authentication method. Disabled
|
|
312
|
+
features remain absent. Tests cover a renamed introspection route, its caller
|
|
313
|
+
authentication and response, a configured ES256-only DPoP list, and disabled
|
|
314
|
+
features. [Failure before correction](../validation/discovery-extensions-red.txt),
|
|
315
|
+
[tests](../validation/discovery-extensions-current.txt).
|
|
316
|
+
|
|
317
|
+
The actual node 9.12.2 OP advertises the introspection endpoint and DPoP algorithms
|
|
318
|
+
when enabled, and omits these when disabled. It does **not** advertise an
|
|
319
|
+
introspection authentication-method list in this fixture. The gem retains the
|
|
320
|
+
upstream list (currently Basic) in both metadata documents; this is additional
|
|
321
|
+
metadata, not an assertion that both responses are byte-for-byte equivalent.
|
|
322
|
+
[Reference](../validation/node-discovery-extensions.txt).
|
|
323
|
+
|
|
324
|
+
The installed-gem DPoP smoke compares those values across both discovery
|
|
325
|
+
documents and now uses the OIDC-discovered introspection URL to verify an opaque
|
|
326
|
+
token's active/cnf/type response. All package smoke fixtures pass.
|
|
327
|
+
[Installed run](../validation/discovery-extensions-package.txt).
|
|
328
|
+
|
|
329
|
+
## Implementation requirements
|
|
330
|
+
|
|
331
|
+
Reuse upstream JWT/JWK primitives and token thumbprint storage where suitable,
|
|
332
|
+
but provide a CIBA-scoped validation boundary with the measured nonempty jti,
|
|
333
|
+
proof freshness, method/URI and proof-key rules. Persist replay claims atomically
|
|
334
|
+
and test concurrent use across requests without changing unrelated OAuth flows.
|
|
335
|
+
Preserve the gem's accepted same-DB rollback behavior explicitly; do not silently
|
|
336
|
+
adopt node's nontransactional consumed-on-failure behavior.
|
|
337
|
+
|
|
338
|
+
Cover initial issuance, confidential refresh, proof-required client policy,
|
|
339
|
+
opaque/JWT token key binding and proof-aware UserInfo before advertising support.
|
|
340
|
+
Malformed keys, private key material, unsupported algorithms, stale/future
|
|
341
|
+
proofs, duplicate proofs and incorrect ath must fail without granting access.
|
|
342
|
+
Configuration/migrations, key-selection scope and nonce policy must be explicit.
|
|
343
|
+
The reference cases above are the initial executable comparison, not completion
|
|
344
|
+
of this workstream.
|
|
345
|
+
|
|
346
|
+
## Alternate endpoint origin and proxy trust
|
|
347
|
+
|
|
348
|
+
A fixed canonical base_url previously made CIBA DPoP compare htu with token_url /
|
|
349
|
+
userinfo_url even when the application served the route at another origin.
|
|
350
|
+
A proof for the canonical endpoint was incorrectly accepted at the alternate
|
|
351
|
+
endpoint, while an actual alternate-origin proof was rejected. The
|
|
352
|
+
[pre-fix test](../validation/dpop-alias-red.txt) reproduces issuance with the wrong
|
|
353
|
+
origin in both opaque and JWT access-token modes.
|
|
354
|
+
|
|
355
|
+
The CIBA boundary now compares against the actual endpoint URL, excluding query
|
|
356
|
+
and fragment as before. It retains a fixed ID Token issuer independently of the
|
|
357
|
+
origin handling the HTTP request. Initial issuance, UserInfo and refresh reject
|
|
358
|
+
cross-origin/cross-path proofs; failed initial proofs do not consume approval or
|
|
359
|
+
create grants. Normal OP DPoP handling outside the CIBA boundary is unchanged.
|
|
360
|
+
|
|
361
|
+
The default `ciba_dpop_endpoint_uri` reconstructs the Rack URL without Forwarded
|
|
362
|
+
or X-Forwarded-* headers. Incoming authority/path and server-provided scheme are
|
|
363
|
+
used; accepting Host names and routing aliases remains the application's job.
|
|
364
|
+
A trusted ingress deployment can override the callback with a validated external
|
|
365
|
+
URL, for example a value placed in an internal Rack environment key by trusted
|
|
366
|
+
middleware. Do not populate that key directly from arbitrary client headers.
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
ciba_dpop_endpoint_uri do
|
|
370
|
+
request.env.fetch("myapp.trusted_external_endpoint")
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
[Reference HTTP evidence](../validation/node-dpop-alias-trust.txt) uses two actual
|
|
375
|
+
listeners sharing one issuer and verifies that forwarded-header spoofing alone
|
|
376
|
+
does not authorize a canonical proof at the other origin. Gem tests cover
|
|
377
|
+
opaque/JWT tokens, wrong origin/path, initial/refresh/UserInfo, unchanged issuer,
|
|
378
|
+
spoofed forwarding headers and an explicit trusted-URL callback.
|
|
379
|
+
[Local regression](../validation/dpop-alias-trust-current.txt).
|
|
380
|
+
The installed smoke additionally exercises alternate-port URLs through Rack and
|
|
381
|
+
verifies ID Token signatures against published JWKS. That Rack fixture alone is not real-TLS evidence. The subsequent
|
|
382
|
+
[combined mTLS/DPoP integration](mtls-reference-contract.md#real-tls-aliases-with-certificate-authentication-and-dpop-tokens)
|
|
383
|
+
now verifies actual TLS aliases. Production reverse-proxy behavior remains separate.
|
|
384
|
+
|
|
385
|
+
The intermediate dpop-alias-installed.txt failed because the new smoke expected
|
|
386
|
+
invalid_token instead of invalid_dpop_proof; its verification code was corrected
|
|
387
|
+
to use published JWKS. It is not a passing artifact run.
|
|
388
|
+
|
|
389
|
+
Final validation: [Ruby3.4 three-DB subset](../validation/dpop-alias-trust-matrix.txt)
|
|
390
|
+
passes21 tests /390 assertions on SQLite, PostgreSQL and MySQL with no
|
|
391
|
+
failures/errors/skips. [Installed artifact](../validation/dpop-alias-trust-installed.txt)
|
|
392
|
+
passes the full smoke harness; alias cases use Rack transport. The earlier full
|
|
393
|
+
391-test Ruby3.3 run predates this correction and is not a full regression of it.
|
|
394
|
+
|
|
395
|
+
## ID Token versus access-token confirmation claims
|
|
396
|
+
|
|
397
|
+
The pinned reference emits no cnf in initial or refreshed ID Tokens for DPoP or
|
|
398
|
+
certificate-bound access tokens. Reference fixtures now verify ID Token signatures
|
|
399
|
+
and assert this absence, while retaining their access-token binding and UserInfo
|
|
400
|
+
checks ([DPoP](../validation/node-dpop-id-token-binding.txt),
|
|
401
|
+
[real TLS mTLS](../validation/node-mtls-id-token-binding.txt)).
|
|
402
|
+
|
|
403
|
+
The gem's CIBA ID Token builder called upstream jwt_claims, which includes the
|
|
404
|
+
access-token sender binding. The [pre-fix test](../validation/id-token-binding-red.txt)
|
|
405
|
+
reproduces inherited cnf in both DPoP and mTLS cases (the same run also contains
|
|
406
|
+
an unrelated local-listener permission error and is not a passing suite).
|
|
407
|
+
The builder now removes that inherited claim in the CIBA ID Token projection,
|
|
408
|
+
just as it already removes token provenance/resource/authorization_details fields.
|
|
409
|
+
Access-token generation, stored proof/certificate thumbprints, introspection cnf,
|
|
410
|
+
and resource proof enforcement retain their existing behavior. Ordinary OIDC
|
|
411
|
+
flows are not modified by this CIBA-scoped correction.
|
|
412
|
+
|
|
413
|
+
The installed smoke checks both initial and refreshed ID Tokens and verifies
|
|
414
|
+
access-token/introspection binding remains present; mTLS cases use actual TLS
|
|
415
|
+
with PKI and self-signed authentication, each in opaque/JWT access-token modes.
|
|
416
|
+
This is separation of automatically inherited sender constraints, not a ban on
|
|
417
|
+
application-defined custom ID Token claims.
|
|
418
|
+
|
|
419
|
+
Final evidence: [Ruby3.4 three-DB subset](../validation/id-token-binding-matrix.txt)
|
|
420
|
+
passes35 tests /729 assertions on each of SQLite/PostgreSQL/MySQL, with no
|
|
421
|
+
failures/errors/skips. [Installed artifact](../validation/id-token-binding-installed.txt)
|
|
422
|
+
passes its full smoke harness, including the new ID Token assertions. This is
|
|
423
|
+
focused regression evidence; the earlier full-suite logs predate this correction.
|