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,40 @@
|
|
|
1
|
+
# Resource indicators: reference behavior and integration boundary
|
|
2
|
+
|
|
3
|
+
2026-09-30. The complete node-oidc-provider 9.12.2 harness now registers two resource servers with separate read/write consent and JWT access tokens. It runs on Node24.21.0 with the built-in memory adapter; the resource information callback uses a fixed registry and performs no network discovery.
|
|
4
|
+
|
|
5
|
+
## Verified behavior
|
|
6
|
+
|
|
7
|
+
| Scenario | Complete reference OP result |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Start with two repeated resource parameters; select A at the Token Endpoint | Access token audience is A and scope is A's approved read permission, not B's write permission. ID Token audience remains client. Both JWT signatures are verified. |
|
|
10
|
+
| Poll selects B when only A was requested, even though saved Grant allows B | invalid_target; the request has already been consumed by the reference's nontransactional lifecycle. |
|
|
11
|
+
| Start requests A, but polling omits resource | With default useGrantedResource=false and UserInfo enabled, issuance selects the OIDC/UserInfo path. Token scope is openid and no resource audience is set. |
|
|
12
|
+
| Relative URI, fragment-bearing URI, or unregistered resource at start | invalid_target. |
|
|
13
|
+
| Introspection of a resource JWT, before and after replay revocation | HTTP 400 unsupported_token_type; the OP does not report active/inactive for this structured token. |
|
|
14
|
+
|
|
15
|
+
[Executable harness](../../test/reference/full-op/lifecycle.mjs), [LTS log](../validation/node-full-op-lts.txt). Existing completion, claims, nonce, expiry and replay scenarios still pass in the same OP run.
|
|
16
|
+
|
|
17
|
+
[RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html#section-2) permits repeated resource parameters, requires absolute fragment-free identifiers, and defines invalid_target. An identifier need not be fetched as a URL. Issued tokens should be audience-restricted; the resource value may be mapped to an audience. Omission policy is an authorization-server choice, not a requirement to pick the first requested target.
|
|
18
|
+
|
|
19
|
+
## Required gem changes
|
|
20
|
+
|
|
21
|
+
The original integration review found global duplicate-key rejection and one scope set shared by access-token creation and the ID Token gate. The optional implementation now addresses these assumptions; see [activation and current limitations](../resources.md). The following remains the design and verification contract, not a claim that all optional policies are complete.
|
|
22
|
+
|
|
23
|
+
1. Permit repeated `resource` values only in this extension; keep duplicate rejection for grant_type, hints, credentials and other scalar parameters. Preserve the original validated requested set separately from token-time selection. Signed request arrays remain out of the current profile.
|
|
24
|
+
2. Store resource permissions on saved consent as a map from identifier to scope set. Requested resources are not proof of approval. Revalidate client eligibility, resource registration, saved consent and revocation before issuance and after transactional hooks.
|
|
25
|
+
3. Keep OIDC scopes and resource scopes separate. Upstream `generate_id_token` requires openid in the grant it receives; a resource-specific access token legitimately has only read/write. Do not solve this by adding openid or all consent scopes to the resource token. Supply the appropriate approved OIDC context to ID Token generation while preserving the access token's narrowed scope and audience.
|
|
26
|
+
4. Persist the selected resource/audience and effective resource scope on the issued token row. JWT and opaque/introspection paths must express the same restriction. A resource token must not gain access to UserInfo merely because a scope string resembles an OIDC scope.
|
|
27
|
+
5. Specify omission policy explicitly. The reference default is the UserInfo path, not automatic selection of the sole requested resource. An application policy may choose a granted resource; multiple candidates require deterministic selection or rejection, never accidental first-element selection.
|
|
28
|
+
6. Add nullable request/consent/token fields with an explicit migration. Old rows carry no resource permission. Activate after every worker enforces the new fields; preserve current scope-only behavior when disabled and do not roll back while restricted tokens remain active.
|
|
29
|
+
|
|
30
|
+
The existing approved same-DB rollback policy still applies. Do not copy reference consumption-after-invalid_target solely because it is observed with the memory adapter. Determine failure timing and retry behavior in the gem and test it explicitly.
|
|
31
|
+
|
|
32
|
+
## Upstream integration review
|
|
33
|
+
|
|
34
|
+
rodauth-oauth 1.7.0 `oauth_resource_indicators` handles repeated form values and can add audiences to JWT/introspection output. Its shown grant validation hooks cover authorization-code and token-exchange-style helpers, not this CIBA saved-consent map. Its JWT audience method also reads request parameters; it does not prove the value was part of a CIBA approval. Enabling that feature alone is therefore insufficient.
|
|
35
|
+
|
|
36
|
+
Additional reference evidence: selecting a requested target with no saved permission returns a token with no scope claim in its JWT and an empty response scope. It does not fall back to OIDC permission. The gem's upstream encoder represents the same empty permission as an empty JWT scope string.
|
|
37
|
+
|
|
38
|
+
JWT introspection follow-up: the initial hypothesis that the reference returns active=true then active=false after revocation was disproved by its HTTP response. `lib/shared/reject_structured_tokens.js` rejects decodable JWT headers before introspection (and revocation) handling. The gem now returns unsupported_token_type for a verified CIBA resource JWT. It preserves upstream handling for non-resource tokens and invalid signatures rather than globally changing all OP flows. Opaque introspection remains the online revocation path. This difference is documented rather than mistaken for complete structured-token parity.
|
|
39
|
+
|
|
40
|
+
The gem now tests request/consent/policy intersection, policy removal in the issue hook, malformed/multiple targets, omission, JWT and opaque audience, UserInfo denial including openid-bearing resource tokens, opaque revocation, signing rollback and additive migration. Follow-up tests add default-resource callbacks, later activation of the upstream resource feature, explicit ID Token claims with resource tokens, and concurrent selection of two different audiences. Node's complete OP confirms opt-in single-target selection, ambiguity rejection and configured default selection. All feature load orders and all claim combinations are not covered. Current reference tests establish a contract subset, not complete RFC8707 or resource-server enforcement.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Signed CIBA authentication requests
|
|
2
|
+
|
|
3
|
+
2026-09-30. Measured against released node-oidc-provider 9.12.2 on Node24.21.0.
|
|
4
|
+
The gem now has an opt-in implementation. This contract records the original
|
|
5
|
+
implementation boundary; [support and remaining verification](../signed-requests.md)
|
|
6
|
+
distinguish tested behavior from unfinished coverage.
|
|
7
|
+
|
|
8
|
+
## Specification boundary
|
|
9
|
+
|
|
10
|
+
[CIBA Core sections 4, 7.1.1 and 7.2](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)
|
|
11
|
+
define asymmetric signed requests, algorithm registration/discovery, and required
|
|
12
|
+
iss/aud/exp/iat/nbf/jti. The request JWT carries authentication parameters;
|
|
13
|
+
client-authentication parameters remain outside it. requested_expiry may be a
|
|
14
|
+
string or number. Encrypted request JWTs are unsupported. Client authentication
|
|
15
|
+
and request-signature verification are separate checks. The former is not
|
|
16
|
+
replaced by a successfully verified request object.
|
|
17
|
+
|
|
18
|
+
## Actual reference evidence
|
|
19
|
+
|
|
20
|
+
The [complete HTTP harness](../../test/reference/full-op/lifecycle.mjs) now enables
|
|
21
|
+
`features.requestObjects` and adds a separate `signed` client, with its own RSA
|
|
22
|
+
key and registered RS256 backchannel request algorithm. The OP signing key is
|
|
23
|
+
different. Existing unsigned clients and all earlier scenarios continue passing.
|
|
24
|
+
[Execution log](../validation/node-full-op-lts.txt).
|
|
25
|
+
|
|
26
|
+
| Case | Measured result |
|
|
27
|
+
|---|---|
|
|
28
|
+
| RequestObjects disabled | No backchannel signing algorithms in Discovery |
|
|
29
|
+
| RequestObjects enabled | Discovery includes RS256; source excludes HS algorithms |
|
|
30
|
+
| Correct signed request, authenticated client | Pending request for customer, requested lifetime 123 seconds |
|
|
31
|
+
| Customer approval then poll | Actual signed ID Token has audience `signed` |
|
|
32
|
+
| Correct signature without client authentication | 401 invalid_client |
|
|
33
|
+
| Registered signed client sends ordinary unsigned parameters | 400 invalid_request |
|
|
34
|
+
| Same signed JWT submitted twice | Two distinct accepted authentication requests |
|
|
35
|
+
| Conflicting outer scope/login_hint | Ignored; values inside JWT determine request |
|
|
36
|
+
| Missing inner scope/hint, values supplied outside | 400 invalid_request; outer values cannot fill missing JWT fields |
|
|
37
|
+
| Missing any of iss/aud/exp/iat/nbf/jti | 400 invalid_request |
|
|
38
|
+
| Wrong issuer/audience/client_id, expired exp or future nbf | 400 invalid_request |
|
|
39
|
+
| Wrong signing key or unknown kid | 400 invalid_request |
|
|
40
|
+
| Nested request/request_uri | 400 invalid_request |
|
|
41
|
+
| requested_expiry number or numeric string | Both accepted with requested lifetime |
|
|
42
|
+
| Future iat with valid exp | Accepted by the default reference checks |
|
|
43
|
+
| Empty-string jti | Accepted by the default reference checks |
|
|
44
|
+
|
|
45
|
+
The reference middleware authenticates first, strips outer authorization
|
|
46
|
+
parameters, processes the signed request and then validates CIBA parameters.
|
|
47
|
+
Request-object errors are remapped to invalid_request. Its default policy
|
|
48
|
+
requires jti but has no one-use request ledger. These measurements are not a
|
|
49
|
+
recommendation to accept empty identifiers or inconsistent timestamps.
|
|
50
|
+
|
|
51
|
+
## Upstream reuse and necessary adaptation
|
|
52
|
+
|
|
53
|
+
rodauth-oauth 1.7.0's `oauth_jwt_secured_authorization_request` handles the browser
|
|
54
|
+
authorization route. Its `validate_authorize_params` may fetch request_uri,
|
|
55
|
+
accepts optional iss/aud checks and overlays JWT fields on the existing request.
|
|
56
|
+
Calling it unchanged would preserve unsigned outer fields and would not impose
|
|
57
|
+
the CIBA claim contract. It also reports authorization-route errors rather than
|
|
58
|
+
the CIBA invalid_request response. Do not enable or change this globally merely
|
|
59
|
+
to add CIBA support.
|
|
60
|
+
|
|
61
|
+
Reuse `oauth_application_jwks` for the authenticated client's registered keys,
|
|
62
|
+
with explicit CIBA signature/algorithm/claim checks. Do not trust a key or URL
|
|
63
|
+
supplied in the JWT header. Before this increment, the CIBA client eligibility check
|
|
64
|
+
rejects any `backchannel_authentication_request_signing_alg`; it must be changed
|
|
65
|
+
along with activation and metadata, not independently.
|
|
66
|
+
|
|
67
|
+
## Implementation contract
|
|
68
|
+
|
|
69
|
+
1. Add an opt-in asymmetric algorithm list, empty by default; publish it only
|
|
70
|
+
when active. Add explicit client-metadata migration/configurable column and
|
|
71
|
+
require the registered algorithm to match the verified JWT. Preserve other
|
|
72
|
+
OAuth routes and client-authentication mechanisms.
|
|
73
|
+
2. Normalize parameters only after client authentication and signature/claim
|
|
74
|
+
validation. With a request object, use inner authentication parameters only,
|
|
75
|
+
as measured in node. Reject nested requests, encryption, unsupported/none/HS
|
|
76
|
+
algorithms and malformed or oversized compact JWTs.
|
|
77
|
+
3. Require the six standard claims, client issuer and OP audience membership.
|
|
78
|
+
Reject invalid time types, future nbf, expired exp, empty jti and future iat.
|
|
79
|
+
Empty-jti/future-iat rejection will be explicit stricter differences from the
|
|
80
|
+
measured default, rather than undocumented assumptions about node.
|
|
81
|
+
4. Convert only defined representations: requested_expiry number/string;
|
|
82
|
+
claims object and authorization_details array to their existing parsers;
|
|
83
|
+
resource string/array to the existing resource validator. Other standard
|
|
84
|
+
authentication fields remain strings. Preserve exactly-one-hint validation.
|
|
85
|
+
5. Do not use the client-assertion replay ledger for request objects. Under the
|
|
86
|
+
node-compatible default, a repeated valid signed request creates a fresh
|
|
87
|
+
request requiring new approval; it neither retrieves nor reuses an earlier
|
|
88
|
+
grant. Document this delivery/retry behavior and retain normal rate controls.
|
|
89
|
+
6. Keep raw request JWTs, client authentication parameters and JOSE metadata out
|
|
90
|
+
of persisted device snapshots and observation events. Only validated CIBA
|
|
91
|
+
inputs enter the existing request/consent lifecycle.
|
|
92
|
+
|
|
93
|
+
Required verification: local negative tests and actual issuance; all existing
|
|
94
|
+
DB/Ruby matrix entries; independent reference scenarios above; installed-gem
|
|
95
|
+
signed-request smoke; Discovery/client metadata and migration tests; signing
|
|
96
|
+
key selection/rotation, request versus client-assertion separation, malformed
|
|
97
|
+
JSON and outer-parameter confusion checks. Remote JWKS transport/cache failures
|
|
98
|
+
and multiple feature-enable orders require explicit coverage, not inference
|
|
99
|
+
from a static in-memory key fixture.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Algorithm acceptance audit
|
|
103
|
+
|
|
104
|
+
The dedicated `test/reference/full-op/signed-algorithms.mjs` runs the released
|
|
105
|
+
provider with explicit RS256/384/512, PS256/384/512 and ES256/384/512 request
|
|
106
|
+
algorithms. All nine accept the registered key and reject a distinct signing
|
|
107
|
+
key. [Node log](../validation/node-signed-algorithms.txt). The gem exercises the
|
|
108
|
+
same algorithm set, a five-part encrypted shape rejection, and numeric-date type
|
|
109
|
+
rejections. Its six signed-request tests pass128 assertions on Ruby4.0.6/SQLite.
|
|
110
|
+
[Gem log](../validation/signed-request-audit-current.txt). No runtime code changed.
|
|
111
|
+
These algorithm cases prove initiation validation, not full issuance with each
|
|
112
|
+
algorithm. Reference Ed25519/EdDSA remains outside the gem's enabled algorithm
|
|
113
|
+
set and is an unresolved alignment difference.
|
data/docs/resources.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Optional resource indicators
|
|
2
|
+
|
|
3
|
+
Resource indicators separate an API's permission from the OIDC/UserInfo permission. This capability is opt-in; the initial poll/login_hint profile does not require it. The client repeats `resource` in the Backchannel request and selects at most one at the Token Endpoint.
|
|
4
|
+
|
|
5
|
+
## Migration and activation
|
|
6
|
+
|
|
7
|
+
Run the explicit additive migration once, after the base schema:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Rodauth::CibaSupport::Schema.add_resources(DB)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It adds nullable TEXT fields `ciba_requests.requested_resources`, `ciba_grants.resources`, and `oauth_grants.ciba_resource` / `ciba_resource_audience`. Custom table names and the request column name are keyword arguments; configure the same request column in `ciba_request_columns`. Existing rows retain their values and have no resource permission.
|
|
14
|
+
|
|
15
|
+
Upgrade every worker before activation. Do not downgrade the enforcing code while resource tokens remain valid. Disabling the flag prevents completion of requests carrying the new resource field, but upgraded code continues to reject resource tokens at UserInfo.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
ciba_resources_enabled true
|
|
19
|
+
ciba_resource_servers(
|
|
20
|
+
"https://api.example.test/data" => {
|
|
21
|
+
scopes: %w[read delete], audience: "https://api.example.test/data"
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Declare these scopes in the OP and client configuration too. The `ciba_resource_server(resource, oauth_application:)` Ruby method can replace the registry lookup for application-specific eligibility. Return a hash with a String `:audience` and Array of String `:scopes`, or nil for an unavailable target. Identifiers must be absolute, fragment-free URIs; neither the gem nor the default lookup fetches these addresses. A resource identifier may map to a different audience.
|
|
27
|
+
|
|
28
|
+
## Consent and issuance
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
grant = rodauth.create_ciba_grant(
|
|
32
|
+
account_id: customer_id, oauth_application_id: client_id,
|
|
33
|
+
scopes: "openid", # OIDC permission, separate from the API permission
|
|
34
|
+
resources: {"https://api.example.test/data" => "read"}
|
|
35
|
+
)
|
|
36
|
+
rodauth.backchannel_result(request_id, grant, auth_time: verified_auth_time)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requested targets are stored independently from saved consent. A modified result hash cannot replace the stored grant. At issuance, the effective API scopes are the intersection of the original requested scopes, saved permission for the selected resource and its current configured scopes; client eligibility is rechecked. Policy lookup occurs after `before_ciba_issue`, inside the issuance transaction. An empty intersection grants no scope; it never falls back to OIDC permission. Resource consent is not inferred by the convenience approval method: use an explicit saved grant for API permissions.
|
|
40
|
+
|
|
41
|
+
When polling omits `resource`, the default follows node-oidc-provider's UserInfo policy: use the saved OIDC scopes. It does not automatically select the only requested API. Override `ciba_use_granted_resource(request_snapshot)` to return true when the application wants to select from the original request's resources. A sole candidate is selected directly. Multiple candidates are passed as a frozen array of frozen strings to `ciba_default_resource(resources, oauth_application:)`; return one original identifier, or nil to retain the OIDC path. The default leaves the array unresolved and returns `invalid_target`. Invalid boolean results raise a configuration error. Neither callback can authorize an unrequested target or replace saved resource consent.
|
|
42
|
+
|
|
43
|
+
JWT access tokens carry the selected audience and narrowed scopes. Opaque tokens persist these fields and expose audience through the upstream introspection feature, when enabled. Resource servers must verify the audience and required scopes. ID Tokens continue to carry the client audience and approved OIDC context, including an `at_hash` of the actual access token. Resource access tokens are rejected at UserInfo, even if their scope includes `openid`.
|
|
44
|
+
|
|
45
|
+
Malformed, unavailable or unrequested selections return `invalid_target`; multiple token-time targets are rejected. Duplicate scalar parameters remain invalid. Maximums are 16 initial resource parameters and 1,024 bytes per identifier. Issuance errors roll back consumption and token writes under the accepted same-DB policy; this intentionally differs from the node reference's nontransactional memory adapter.
|
|
46
|
+
|
|
47
|
+
## Evidence and limits
|
|
48
|
+
|
|
49
|
+
[HTTP tests](../test/ciba_resources_test.rb) verify separate scopes/audiences, mapped audiences, stored-consent integrity, policy changes in hooks, omission, malformed targets, signing rollback, opaque introspection/revocation, JWT/opaque UserInfo denial and legacy migration. [Reference contract](research/resource-reference-contract.md) records observations from the complete node OP.
|
|
50
|
+
|
|
51
|
+
Additional integration tests cover omission callbacks, explicit ID Token claims together with API scope/audience (including at_hash), simultaneous polling for different audiences and later activation of upstream `oauth_resource_indicators`. CIBA's saved audience takes precedence over that feature's request-based JWT audience. Resource JWTs are rejected at UserInfo. Introspection of a verified CIBA resource JWT returns HTTP 400 `unsupported_token_type`, matching the node reference's rejection of structured tokens. This is unchanged by replay, grant revocation or deletion of the token row. It avoids presenting a stateless JWT decode as an authoritative active/revoked result. Signature and issuer validation remain enabled; tests reject a modified signature and a different issuer.
|
|
52
|
+
|
|
53
|
+
Use opaque access tokens plus introspection when resource servers need the OP's current revocation decision. JWT offline verification does not detect a later grant revocation. The reference rejects all structurally recognizable JWTs before verification; this gem restricts the new error to verified CIBA resource tokens, preserving upstream behavior for other OAuth tokens and invalid signatures. This is an explicit integration difference, not a CIBA Core requirement.
|
|
54
|
+
|
|
55
|
+
This is not complete resource-indicator parity: all upstream feature load orders and all claims/resource combinations have not been tested. For empty API permission, node omits the JWT scope claim while this gem's upstream encoder emits an empty string; both token responses explicitly carry an empty scope.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# CIBA初版:要件・コード・セキュリティレビュー
|
|
2
|
+
|
|
3
|
+
## JWT mTLS introspection and installed rotation (2026-09-30)
|
|
4
|
+
|
|
5
|
+
The installed HTTPS rotation fixture exposed missing certificate cnf in JWT
|
|
6
|
+
introspection. The correction resolves the signed CIBA issuance marker to an
|
|
7
|
+
unrevoked, unexpired row and matches client, subject and certificate digest
|
|
8
|
+
before reporting binding. Opaque/JWT expiry/revocation and cnf regression cases
|
|
9
|
+
pass on all three DBs. The isolated fixture CA enables actual verified HTTPS
|
|
10
|
+
JWKS reads; different certificates with the same key cannot substitute for old
|
|
11
|
+
or new token bindings. Failed retrieval creates no pending request; restoration
|
|
12
|
+
recovers authentication. [Evidence and limits](research/mtls-reference-contract.md#installed-https-certificate-rotation-and-jwt-introspection).
|
|
13
|
+
This remains implementer review, without a multi-process or production proxy audit.
|
|
14
|
+
|
|
15
|
+
## Remote mTLS certificate lookup (2026-09-30)
|
|
16
|
+
|
|
17
|
+
TLS client authentication now uses a distinct lookup flag, ensuring remote
|
|
18
|
+
status/JSON failure reports invalid_client_metadata rather than the
|
|
19
|
+
post-authentication signed-request error. The same guarded HTTP destination,
|
|
20
|
+
size and timeout controls remain active; ensure restores lookup state.
|
|
21
|
+
Certificate mismatch cannot trigger an unbounded refetch. Tests cover exact
|
|
22
|
+
certificate replacement (including a different certificate with the same key),
|
|
23
|
+
repeated retrieval failure without persistence, configured transport-boundary
|
|
24
|
+
refusal and recovery. Node's failed refresh can keep previously cached keys
|
|
25
|
+
fresh; the gem deliberately keeps its rejection until successful retrieval.
|
|
26
|
+
[Measured contract](research/mtls-reference-contract.md#remote-x5c-rotation-and-retrieval-failures).
|
|
27
|
+
This is implementer review, not an independent audit or multi-process proof.
|
|
28
|
+
|
|
29
|
+
## mTLS installed-artifact TLS verification (2026-09-30)
|
|
30
|
+
|
|
31
|
+
The installed gem now passes real WEBrick/Net::HTTP TLS checks for PKI and
|
|
32
|
+
self-signed authentication with opaque and JWT access tokens. Certificate input
|
|
33
|
+
comes from the TLS peer, overriding no client-provided server variables; PKI
|
|
34
|
+
chain verification uses the application trust store and DN policy. Header-only
|
|
35
|
+
and absent certificates fail, a foreign same-DN certificate fails authentication,
|
|
36
|
+
and a same-key/different certificate fails bound UserInfo. The HTTPS client
|
|
37
|
+
verifies the server and hostname. The fixture explicitly permits self-signed
|
|
38
|
+
peers at the listener so the application can validate the selected method.
|
|
39
|
+
This does not verify arbitrary reverse-proxy or trust-store integration.
|
|
40
|
+
[Full scope and evidence](research/mtls-reference-contract.md#installed-gem-with-a-ruby-tls-server).
|
|
41
|
+
|
|
42
|
+
## Initial CIBA mTLS correction, deployment checks open (2026-09-30)
|
|
43
|
+
|
|
44
|
+
CIBA now requires an explicit trusted peer-certificate callback and storage at
|
|
45
|
+
startup. PKI chain/subject callbacks default to deny; self-signed auth compares
|
|
46
|
+
registered certificate DER digests. The grant stores the actual certificate
|
|
47
|
+
digest, and UserInfo requires it after normal live-token verification. Missing
|
|
48
|
+
required certificate and dual DPoP binding are rejected before nested persistence
|
|
49
|
+
so approval/proof/token state rolls back. Local opaque/JWT regression tests
|
|
50
|
+
cover these boundaries. Real Ruby TLS transport, DCR and other integration
|
|
51
|
+
combinations remain open. The following describes the reproduced baseline.
|
|
52
|
+
|
|
53
|
+
The actual gem Rack probe issues a PKI-authenticated certificate-bound token,
|
|
54
|
+
but UserInfo accepts it without a certificate. The stored binding hashes the
|
|
55
|
+
public JWK rather than the certificate DER; registered self-signed x5c auth
|
|
56
|
+
returns500. Forwarded certificate headers are accepted without an explicit
|
|
57
|
+
trusted-proxy policy in the fixture. These are measured gaps, not a complete
|
|
58
|
+
assessment of all deployments. The reference OP's real TLS test rejects absent,
|
|
59
|
+
header-only and same-key/different-certificate presentations. mTLS remains
|
|
60
|
+
disabled by default and must not be claimed as implemented through upstream
|
|
61
|
+
feature activation. [Evidence and required work](research/mtls-reference-contract.md).
|
|
62
|
+
|
|
63
|
+
## DPoP introspection boundary (2026-09-30)
|
|
64
|
+
|
|
65
|
+
Authenticated introspection now retrieves opaque CIBA DPoP tokens without
|
|
66
|
+
requiring possession of the token's key, matching the reference OP. Exact token
|
|
67
|
+
hash, CIBA type, expiry and revocation remain conjunctive predicates. Responses
|
|
68
|
+
report the stored `cnf.jkt` and DPoP type; inactive tokens reveal no binding.
|
|
69
|
+
Wrong client credentials still fail. Tests preserve unrelated OAuth lookup and
|
|
70
|
+
verify no introspection proof is claimed. This is OP metadata for an authorized
|
|
71
|
+
caller, not resource authorization; caller disclosure policy and external proof
|
|
72
|
+
enforcement remain application responsibilities.
|
|
73
|
+
[Evidence and scope](research/dpop-reference-contract.md#opaque-token-introspection-binding).
|
|
74
|
+
|
|
75
|
+
## Partial correction: upstream DPoP integration (2026-09-30)
|
|
76
|
+
|
|
77
|
+
The baseline upstream-only integration rejected random proof identifiers while
|
|
78
|
+
accepting hour-old proofs with a custom identifier and reusing them for another
|
|
79
|
+
approved CIBA issuance. The new CIBA token-endpoint boundary fixes those paths:
|
|
80
|
+
signature/key/claim validation precedes issuance, and a client-scoped,
|
|
81
|
+
domain-separated digest is claimed in the same transaction as token insertion.
|
|
82
|
+
Unique-key conflicts raise through the outer transaction, preserving approval;
|
|
83
|
+
signing failures also roll back the claim. Invalid private/malformed keys,
|
|
84
|
+
alg/typ/crit headers and tampered signatures are rejected before persistence.
|
|
85
|
+
Other OAuth grant validation is delegated unchanged.
|
|
86
|
+
|
|
87
|
+
UserInfo now validates CIBA proofs at `authorization_token`, because the upstream
|
|
88
|
+
OIDC route bypasses the general resource authorization method. It verifies ath,
|
|
89
|
+
the proof key, signed-token provenance and the saved issuance key before claiming
|
|
90
|
+
replay. CIBA-only dataset predicates also repair upstream's broad OR condition,
|
|
91
|
+
which otherwise lets a matching key bypass revocation, expiry or token lookup.
|
|
92
|
+
Opaque/JWT negative cases and concurrent UserInfo replay are tested. Unrelated
|
|
93
|
+
OAuth flows retain upstream behavior; this is not a global fix of that feature.
|
|
94
|
+
|
|
95
|
+
Nonce validation now permits old proof iat only with a valid server challenge.
|
|
96
|
+
Those replay records expire relative to validation time, so cleanup cannot
|
|
97
|
+
immediately reopen replay. The explicit 32-byte issuer secret and required-nonce
|
|
98
|
+
configuration are validated at startup; opaque/JWT tests cover expiry, rotation
|
|
99
|
+
and cleanup followed by replay. Cluster clock/secret management remains an
|
|
100
|
+
application deployment responsibility.
|
|
101
|
+
|
|
102
|
+
External resource integration and broader combinations remain
|
|
103
|
+
open, so this is not complete DPoP support. The default configuration does not
|
|
104
|
+
enable DPoP. Client authentication and customer approval are still required.
|
|
105
|
+
[Reference and implementation boundary](research/dpop-reference-contract.md).
|
|
106
|
+
|
|
107
|
+
## Revocation provenance and legacy formats (2026-09-30)
|
|
108
|
+
|
|
109
|
+
New opaque CIBA ATs reserve `ciba_at_`; this selects client authentication and unknown-token success, never ownership or a DB mutation target. New JWTs always carry the signed issuance ID; UserInfo now requires that exact row even without optional explicit claims. The legacy test reconstructs the historical unmarked JWT, rather than treating current issuance as a legacy fixture.
|
|
110
|
+
|
|
111
|
+
At revocation, unverified JWT markers select only a rejecting validation path. OP signature/issuer/at+jwt type are checked using configured keys before returning unsupported_token_type. Expiration is irrelevant to this rejection-only provenance check and remains enforced by normal consumption. Forged signature, wrong issuer/type and alg=none produce invalid_request without state changes. This intentionally has a narrower scope than node's header-only rejection of all structured JWTs, preserving other OAuth flows. Row-cleaned legacy unprefixed opaque tokens and unmarked old JWTs remain upstream cases.
|
|
112
|
+
|
|
113
|
+
## Opaque access-token revocation (2026-09-30)
|
|
114
|
+
|
|
115
|
+
Saved opaque CIBA tokens select the new path by their stored digest/type, not the untrusted hint. Client authentication remains mandatory even with a browser session. Authorization and validity are rechecked under Grant→token locks. Revocation preserves consent but invalidates related access tokens and deletes old request/refresh sources; otherwise refresh could recreate access or old-request replay could revoke the retained consent. Hook failure restores all these changes. Tests cover cross-client isolation, hints, explicit consent reuse for a new request, concurrent refresh and ordinary OAuth route preservation. JWT rejection and unknown tokens after row cleanup remain separate gaps; this is not full revocation parity.
|
|
116
|
+
|
|
117
|
+
## Refresh lifecycle and hook boundary (2026-09-30)
|
|
118
|
+
|
|
119
|
+
Refresh sources store digests and original permission context separately from access-token rows. Current consent/client/account/capabilities are checked at each update and again after the transactional before hook. Same-source reentrant calls are rejected. Hook/signing failures roll back rotation and issuance; observations wait for outer commit and contain no token or digest. Review moved basic scope parameter validation ahead of the hook to avoid committing hook effects on an immediate parameter error.
|
|
120
|
+
|
|
121
|
+
Tests cover cross-client isolation, age-based rotation, concurrent replay revocation, grant-wide refresh-token revocation, expiry cleanup retaining unexpired consumed records, and opaque/JWT claims/RAR updates under fixed test policies. Ordinary OAuth refresh still follows upstream. Access-token-initiated grant-wide revocation and installed-package refresh smoke remain outstanding. This is implementer review, not an independent security audit or a claim of complete reference parity.
|
|
122
|
+
|
|
123
|
+
## Ping in-flight behavior and adverse transport (2026-09-30)
|
|
124
|
+
|
|
125
|
+
Barrier-based tests show automatic completion can wait for a receiver while another connection retrieves the saved result; no DB transaction is active in the send callback. Two explicit retries can send the same identifier concurrently while collection succeeds once. Both sends may finish after consumption, later retries stop, and duplicate collection still triggers replay revocation. Receivers therefore need identifier deduplication before token collection, not only idempotent HTTP acknowledgement.
|
|
126
|
+
|
|
127
|
+
Installed-gem smoke now tests actual TLS rejection without the temporary CA trusted; no credentials reach the receiver and approval remains saved. A deliberately stalled receiver triggers a transport timeout, after which explicit retry and collection work. This can duplicate a notification already received before timeout. Signed-request/user-code/ping composition rejects outer credential replacement. No production code changes were needed for these cases. Process crashes, multi-process delivery and other TLS failure variants remain unverified; this is not independent audit.
|
|
128
|
+
|
|
129
|
+
## Ping completion and private delivery data (2026-09-30)
|
|
130
|
+
|
|
131
|
+
The original auth_req_id and client notification credential are stored only in a separate delivery table, inserted with the pending request and deleted by request cleanup cascade. Public APIs, hook/device snapshots and events do not contain those fields. This is separation from application-facing snapshots, not at-rest encryption; database privileges/backups remain deployment responsibilities.
|
|
132
|
+
|
|
133
|
+
Delivery runs only after the outermost commit. Rollback sends nothing; failed delivery raises after saved approval/denial, with a separate retry method. Endpoint snapshots prevent sending the credential to a newly registered destination. The current client must still be eligible. Sends use the bounded destination-checked HTTP transport and accept only 200/204. The installed artifact was exercised with real TLS, 503/302 and explicit retry. Dedicated concurrent lifecycle/delivery tests and adverse TLS/timeout cases remain unverified. Primary-implementer review, not independent audit.
|
|
134
|
+
|
|
135
|
+
## Outbound destination checks (2026-09-30)
|
|
136
|
+
|
|
137
|
+
The node comparison exposed special-use-IP protection absent from the gem's signed-request key fetching. A dedicated CIBA transport now rejects private/loopback/link-local, multicast, mapped IPv4 and reference special-use ranges. All resolved addresses must pass policy; Net::HTTP connects to the selected numeric IP while keeping the hostname for TLS/Host. Ambient proxies are disabled, redirects are not followed, and response bytes/total duration are bounded. Static registered JWKS is unaffected; unrelated OAuth HTTP paths remain upstream-owned.
|
|
138
|
+
|
|
139
|
+
Tests prove default refusal before a loopback receiver sees a request, bounded response reading, mapped-address cases, mixed DNS rejection and the pinned-IP/retained-hostname client configuration. Existing remote-key tests explicitly allow their single loopback address. Production TLS errors, live DNS rebinding and multi-address fallback remain limitations; see [transport contract](outbound-http.md). This is primary-implementer review, not independent audit.
|
|
140
|
+
|
|
141
|
+
## User-code application policy (2026-09-30)
|
|
142
|
+
|
|
143
|
+
Opt-in requires an explicit verifier. The application receives the resolved eligible account and authenticated client, before transaction hooks, request storage and device dispatch. Only true accepts; nil/false fails closed. Missing-code policy remains application-owned, as in node. Invalid shapes/oversized codes are rejected before the callback, and unknown/closed accounts do not invoke it. Unexpected callback failures create no pending work.
|
|
144
|
+
|
|
145
|
+
Tests verify raw code exclusion from request rows/device snapshots/events, signed-inner versus outer-code isolation, new approval and metadata. The reference OP independently tests missing/wrong code before dispatch and issuance after separate approval. Demo policies are fixtures only; production code provisioning, storage and brute-force controls are not implemented by the gem. Primary-implementer review, not independent audit.
|
|
146
|
+
|
|
147
|
+
## Remote request-signing keys (2026-09-30)
|
|
148
|
+
|
|
149
|
+
Actual loopback HTTP tests reproduce upstream's registered JWKS cache behavior: max-age prevents a new-kid refresh, no-cache refetches, and explicit cache invalidation permits the new key while rejecting a removed key. An attacker-controlled jku/embedded jwk does not replace registered keys. Malformed/empty JSON, HTTP 503 and connection refusal create no pending requests.
|
|
150
|
+
|
|
151
|
+
The tests exposed upstream's HTTP failure path returning 401 after client authentication had already succeeded. A scoped, ensure-cleared lookup flag now maps that path to invalid_request; common network exceptions are mapped at the same boundary. Wrong client credentials still return 401 and no other request paths enable the flag. This is an explicit error-policy adaptation, not exact node error-code parity. Production TLS/DNS/redirect behavior and distributed cache invalidation remain untested. This is primary-implementer review, not independent audit.
|
|
152
|
+
|
|
153
|
+
## Signed request boundary (2026-09-30)
|
|
154
|
+
|
|
155
|
+
Client authentication completes before request verification. The signing algorithm is pinned to client registration and the enabled asymmetric list. Only registered client JWKS is consulted; kid/alg/use/key_ops constrain candidate keys. An OP signing key does not validate a client request. Duplicate JSON keys and invalid UTF-8 are rejected before normalization, and standard claim types are checked explicitly after signature verification.
|
|
156
|
+
|
|
157
|
+
The original form body is not merged into verified protocol inputs. Tests prove outer scope/hint/resource cannot replace or fill inner values, and requested claims/resources/RAR reach existing validators. A resource-array integration test exposed the existing parser's scalar Rack parameter expectation; normalization now preserves the complete verified resource list while providing its scalar representation to that parser. No raw request JWT is persisted into the device snapshot. A repeated signed request deliberately creates new pending work, matching node; it is not a one-use client assertion. New approval remains mandatory.
|
|
158
|
+
|
|
159
|
+
Five environments pass 160 tests; installed-gem opaque/JWT examples use independently signed requests. Real remote JWKS failures/rotation, all configuration orders and algorithm variants remain unverified. This is the primary implementer's review, not an independent audit.
|
|
160
|
+
|
|
161
|
+
## ID Token hint validation (2026-09-30)
|
|
162
|
+
|
|
163
|
+
Dedicated verification ignores expiration only for hints; the ordinary JWT decoder remains unchanged. Algorithm comes from the authenticated client's configuration and keys from the OP. Signature/issuer/audience precede subject resolution. Unknown keys, wrong algorithm/client/issuer never reach the resolver. Raw hints are removed before storage and snapshots. Retained keys and kid selection are tested.
|
|
164
|
+
|
|
165
|
+
An adversarial test found ruby-jwt accepted a string nbf through numeric coercion. The dedicated verifier now explicitly checks iat/exp/nbf types. A different azp, at+jwt type and critical JOSE extensions are rejected; these stricter differences from the reference are documented. HMAC follows the actual upstream issuer's OP key source, with a client-secret forgery rejection test. Age defaults to unrestricted as in node, with an optional maximum. Encrypted/pairwise hints remain incomplete. This is the primary implementer's review, not an independent audit.
|
|
166
|
+
|
|
167
|
+
## login_hint_tokenの境界(2026-09-30)
|
|
168
|
+
|
|
169
|
+
無効を既定とし、有効化には専用resolverを必須にした。認証済みclientをcallbackへ渡し、入力hintの排他・長さ・型を検証する。アプリが返したaccountは既存の有効性と承認account/client一致チェックを通る。token endpointへ別hintを送っても保存済みユーザーは変わらない。
|
|
170
|
+
|
|
171
|
+
raw hint tokenはresolver後に要求の保存対象から除き、DB・AD snapshot・観測eventに含まれないことを検証した。元のHTTPリクエストやアプリ独自ログまで消去する保証はない。トークン形式はアプリ所有で、fixtureは固定参照値だけを解決するため、本番JWT等の検証器を検証したものではない。nodeは元のparamsを保存するため、raw tokenを残さない点は明示した保存方式の差である。
|
|
172
|
+
|
|
173
|
+
## 新形式の承認を結び付けた旧要求の無効化境界(2026-09-30)
|
|
174
|
+
|
|
175
|
+
RAR導入前の要求に、導入後に作ったRAR承認を結び付け、RARを無効化すると、要求側の新列がnullのため発行前チェックを通り、承認のRARを無視した基本tokenが発行されることを再現した。保存済み承認の機能列も検証対象に追加し、claims/resourceにも同じ境界を適用した。機能列が要求または承認のどちらかに記録されている限り、対応機能の無効化後はinvalid_grantとなり、消費と発行をrollbackする。
|
|
176
|
+
|
|
177
|
+
RARの同時pollでは異なるnarrowingを別DB接続から競合させ、勝者一件の内容だけが保存されること、再引換え検知後は失効することを検証。発行と取消しの競合でも、完了後に有効tokenやintrospectionのauthorization_detailsが残らないことを確認した。参照OPでもopaque RAR tokenの再引換え後にactive=falseとなる。全てのスケジューリングを証明するものではなく、RARのcompletion競合は別途残る。
|
|
178
|
+
|
|
179
|
+
## RARの受付・発行境界(2026-09-30)
|
|
180
|
+
|
|
181
|
+
登録型・クライアント許可型・JSON形状・型固有validatorを入力と保存済み承認とpolicy出力に適用する。発行policyには凍結済みの要求/承認/取得時要求と検証済みresourceを渡す。保存GrantをIDで再取得するため、呼出し側snapshotを書き換えても承認は増えない。発行直前hookでclientの許可型が変われば拒否し、要求消費とtoken作成をrollbackする。
|
|
182
|
+
|
|
183
|
+
RARはtoken response・resource JWT・opaque introspectionだけに出力し、ID Tokenから除く。無効化後のRAR要求はinvalid_grant、policy未設定は構成エラー。5環境で141テスト成功し、配布gemの複合フローも検証した。
|
|
184
|
+
|
|
185
|
+
型固有の包含関係やresourceへの割当は必須アプリpolicyの責務で、gemの形状検証だけでは保証しない。RAR専用の同時実行、introspectionでの追加秘匿policy、全feature有効化順の検証は残る。このレビューは主担当によるもので、独立監査ではない。
|
|
186
|
+
|
|
187
|
+
## resource JWT introspectionの参照比較による修正(2026-09-30)
|
|
188
|
+
|
|
189
|
+
前段階ではresource JWTを署名検証後にactiveとして返したが、再引換えで保存Grantが失効してもactive=trueが続くことを再現した。nodeの実OPとの比較では、active/inactiveを返すという仮説が誤りで、構造化JWTは最初からHTTP400 unsupported_token_typeだった。
|
|
190
|
+
|
|
191
|
+
gemも検証済みCIBA resource JWTについて同エラーを返すように修正した。発行直後、再引換え後、明示取消し後、発行行削除後をテストした。署名・issuerの検証は維持し、opaqueの失効結果と既存OAuthフローの扱いは保持する。nodeはJWT構造だけで全JWTを拒否するため、対象の限定はgem側の意図的な接続差である。以下の「JWTをintrospectionで説明する」記述は修正前の履歴。
|
|
192
|
+
|
|
193
|
+
## resource併用時のaudience修正(2026-09-30)
|
|
194
|
+
|
|
195
|
+
上流 `oauth_resource_indicators` を後から有効にすると、CIBAで保存したmapped audience `service-b` が、要求のresource URI配列に置き換わることを実際の署名済みJWTで再現した。CIBAの保存済みaudienceを最後に適用するwrapperへ移し、ID Tokenのclient audience、resource tokenのintrospection、UserInfo拒否をHTTP経由で検証した。OPのintrospectionはAPIの宛先ではないため、このCIBA tokenではaudienceの一致を要求せず、署名・issuer等の検証を維持する。改ざん署名・別issuerの拒否をテストした。JWTの即時失効保証を追加したものではない。
|
|
196
|
+
|
|
197
|
+
同時に異なるresourceを選択したpollは、一件だけ発行し、後続の再引換え検知でそのGrant/tokenを失効する。claims併用では明示許可されたemailだけがID Tokenへ入り、API tokenのscope/audienceは別に維持され、at_hashが実際のAPI tokenに一致する。
|
|
198
|
+
|
|
199
|
+
## optional resource対応のレビュー(2026-09-30)
|
|
200
|
+
|
|
201
|
+
主担当が、要求先を承認と誤認しないこと、保存済みGrantを再読込すること、発行hook後にresource policyを再取得すること、API scopeとID TokenのOIDC contextを分離することを確認した。JWT署名付きのresource識別子とopaque token行の識別子を使い、openid scopeを含むAPI tokenもUserInfoで拒否する。機能フラグを後から無効化してもこの拒否は維持する。
|
|
202
|
+
|
|
203
|
+
実装途中のHTTP試験で、introspection機能を後から有効化するとaudience追加メソッドが上書きされることを確認した。prependによる専用wrapperへ移し、opaqueのaudienceと取消しを実際のintrospection応答で検証した。未知・未要求resourceと署名失敗は要求消費/token保存をrollbackする。
|
|
204
|
+
|
|
205
|
+
5環境で各125テスト成功。これは独立した第三者監査ではない。resource専用の同時実行試験、claims併用、upstream resource featureとの組合せは未検証で、完全なresource対応とはしない。[対応範囲](resources.md)。
|
|
206
|
+
|
|
207
|
+
## optional claims追加後のレビュー(2026-09-30)
|
|
208
|
+
|
|
209
|
+
主担当が追加したclaims/UserInfoの接続を再レビューし、旧JWTへの許可の混入を再現・修正した。旧JWTの発行行が削除され、同じaccount/clientの別発行行にemail許可が残っていると、UserInfoの検索結果から旧JWTへその許可が使われていた。JWTの署名を改ざんせず、実際に機能追加前に発行したトークンで再現した。
|
|
210
|
+
|
|
211
|
+
修正後は、CIBAの明示claim projectionを読み込む前に、提示トークンとの厳密な発行ID結び付けを必須にした。結び付けのない旧JWTは既存のscopeベース経路を使用し、新機能の明示許可を引き継がない。`test_legacy_jwt_never_borrows_claim_consent_from_another_issuance`で修正前のemail漏出を確認後、修正後のemail非出力・属性getter非実行を検証した。旧JWTのDB行単位の取消し保証を追加したわけではない。
|
|
212
|
+
|
|
213
|
+
ローカル全体は117 tests / 896 assertions成功。以下はそれ以前の初版レビュー記録であり、今回の追加機能すべての独立監査を意味しない。
|
|
214
|
+
|
|
215
|
+
2026-09-29。軽量サブエージェント2体が要件整理と不足テストを担当し、主担当がコード・セキュリティレビュー、修正、再レビューを行った。対象はpoll/login_hintの初版と、実際に有効化して試験した上流クライアント認証の接続部分。Jevの点数は合否基準に使っていない。
|
|
216
|
+
|
|
217
|
+
## 再現して修正した問題
|
|
218
|
+
|
|
219
|
+
| 問題 | 修正と証拠 |
|
|
220
|
+
|---|---|
|
|
221
|
+
| private_key_jwtでiss不一致、exp/jti欠落、同じassertion再利用が受理される | OIDC Core §9の必須claim・iss/sub/client_idの一致を検証し、DBのunique digestで一回限りにする。`test_jwt_client_assertion_requires_issuer_expiry_and_jti`, `test_jwt_client_assertion_is_single_use` |
|
|
222
|
+
| Backchannelに別grant用grant_type/assertionを渡すと、登録済みのclient authenticationではなくJWT authorization grantの経路へ入る | Backchannelではそのgrant分岐を使わず、通常のclient authenticationを必須にする。余分なパラメータがあっても正しく認証した要求は通る。`test_backchannel_ignores_other_grant_parameters_without_bypassing_client_authentication` |
|
|
223
|
+
| 壊れたJWTが認証失敗ではなく500になる | CIBAの認証境界で形式を検証し401 invalid_clientへ。開始・Token両Endpointの負例を検証。`test_malformed_assertions_are_authentication_errors_at_both_ciba_endpoints` |
|
|
224
|
+
| カスタムerror_descriptionに非ASCII等がそのまま出る | 許可文字外の任意説明は省略し、status/errorは保持する。許可ASCIIは保持。`test_authentication_error_description_keeps_allowed_ascii_and_omits_unsafe_text` |
|
|
225
|
+
| scope欠落がinvalid_scopeになる | 欠落はinvalid_request、存在する不正scopeはinvalid_scopeへ区別。`test_scope_syntax_errors_are_invalid_scope` |
|
|
226
|
+
| slow_downのintervalが32bit上限で飽和し、5秒以上増加しない | 保存列をBignumにし、要求時の上限と実行中の増分を分離。`test_slow_down_does_not_saturate_at_the_initial_interval_limit` |
|
|
227
|
+
| 矛盾するform構造によるRack解析例外が500になる | 解析例外をinvalid_requestへ変換。`test_conflicting_form_parameter_shapes_return_invalid_request` |
|
|
228
|
+
|
|
229
|
+
| offline_accessを除外したのに実効scopeを応答に返さない | CIBA token responseに許可済みscopeを含める。RFC6749 §5.1の、要求と許可scopeが異なる場合の返却条件を確認。`test_reduced_scope_is_reported_when_offline_access_is_not_granted` |
|
|
230
|
+
|
|
231
|
+
## 追加の防御と再レビュー
|
|
232
|
+
|
|
233
|
+
- CIBA clientのJWTは、BackchannelとToken、および同じクライアントの非CIBA grant間でreplay ledgerを共有する。別クライアントの通常フローにこの制約を一律適用しない。
|
|
234
|
+
- ledgerのunique insertはsavepoint内。別接続からの同時再利用で1件のみ成功し、拒否を外側transactionで処理しても他のDB操作が続行できることを検証した。新しいテーブルには生のJWT・jti・secretを保存しない。
|
|
235
|
+
- client JWKS未設定時にOPの署名鍵へフォールバックしない。HS256/384/512の共有鍵長を検証。これらはソースレビューで認証の境界を確認し、負例テストを追加した。
|
|
236
|
+
- JWTのaudは文字列・文字列配列を受理する。CIBA開始時はissuer/Token URL/Backchannel URLの3種類を受理し、CIBA grant の Token 時は issuer または Token request URL を検証する。
|
|
237
|
+
- identity mismatch、認証前・拒否後・期限切れの発行禁止、署名失敗・hook失敗のrollback、二重poll、approve/deny競合、outer rollback後の観測抑止は既存テストを含め再実行した。
|
|
238
|
+
- 要件表の架空のテストクラス名、章単位の広すぎる根拠、任意と必須の混同を主担当レビューで差し戻し、原仕様と実在メソッドへ対応付け直した。
|
|
239
|
+
|
|
240
|
+
仕様根拠: [CIBA Core §7.1/§7.2/§11/§13](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)、[OIDC Core §9](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)。[OAuth 2.0 §5.1](https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1)。RFC7523単体のreplay対策は任意だが、今回のOIDCクライアント認証にはOIDC §9のjti/single-use条件が適用される。
|
|
241
|
+
|
|
242
|
+
## 検証結果と残る限界
|
|
243
|
+
|
|
244
|
+
`mise exec ruby -- ruby test/run.rb --seed 41025`:**76 tests / 563 assertions、failure/error/skipなし**。macOS arm64 / Ruby4.0.6 / SQLite。ログはリポジトリの `docs/validation/requirements-review-tests.txt`。
|
|
245
|
+
|
|
246
|
+
今回再現した不具合は修正後の回帰テストを通過し、上記の範囲で追加の既知の未修正不具合は残していない。これは安全性や全仕様適合を保証する意味ではない。
|
|
247
|
+
|
|
248
|
+
- Docker socketは現在の実行権限でpermission denied。新しいreplay tableとinterval列を含む現行コードのPostgreSQL/MySQL検証は未実施。以前の46テスト版のmatrix成功を、この変更の証拠として流用しない。
|
|
249
|
+
- 159は人手で整理した評価単位であり、参照先OIDC/RFCまで含めた全規範文の網羅性証明ではない。要件表は設定・モード・責務の条件と一緒に読む。
|
|
250
|
+
- mTLS、追加の上流拡張や署名algorithm全組合せ、別middleware構成、複数プロセスの実環境、長時間負荷、failoverは未検証。署名検証は上流/JWTライブラリに依存する。
|
|
251
|
+
- 本人確認、承認権限、CSRFを伴う本番UI、逆proxyの信頼設定、通知・監査の耐久配送は導入アプリの契約として残る。hookからのRack halt/throwは禁止し、失敗は例外で通知する。
|
|
252
|
+
- 認証成功応答を外側transactionのcommit前に返さない。JWTは業務要求が失敗しても消費される場合があるため、retryには新しいassertionを使う。live ledger行の早期削除はしない。
|
|
253
|
+
|
|
254
|
+
今回の依頼は要件整理・不足テスト・修正まで。`pkg/`の配布gemは今回の修正前の成果物であり、再公開候補としては使わない。公開前のDB matrix、配布物の再ビルドとclean install確認は次の工程として残す。
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
## 登録した client assertion アルゴリズムの制限
|
|
258
|
+
|
|
259
|
+
動的登録で `token_endpoint_auth_signing_alg=none` が201となることと、
|
|
260
|
+
RS512指定のclientがRS256 assertionで認証されることを別々に再現した。
|
|
261
|
+
登録時は方式・型・広告済みアルゴリズムを検証し、認証時は保存値とheaderを
|
|
262
|
+
比較したうえで、private_key_jwtのdecoderにも選択したアルゴリズムを渡す。
|
|
263
|
+
署名の正しさだけで登録制約を満たしたと判断しない。
|
|
264
|
+
[登録の再現](validation/registration-algorithm-red.txt)、
|
|
265
|
+
[認証時の再現](validation/registration-algorithm-runtime-red.txt)。
|
|
266
|
+
node比較はRS512を明示的に有効化したfixtureである。HMAC等の全組合せ、
|
|
267
|
+
JWK構造そのものの登録時検証は、この修正の検証範囲には含まれない。
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
## CIBA client assertion のリモート鍵取得
|
|
271
|
+
|
|
272
|
+
動的登録した private_key_jwt の JWKS 取得が、署名付き要求で使用している
|
|
273
|
+
送信先アドレス制限を迂回する経路を再現した。試験では登録URLポリシーだけを
|
|
274
|
+
対象のlocalhost fixtureに緩め、取得時の既定アドレス拒否が効くかを独立に確認。
|
|
275
|
+
修正後は既存のIP固定・proxy無効・deadline・応答上限を持つtransportを使い、
|
|
276
|
+
取得/JSON解析失敗は認証拒否とする。管理更新で別URLに切り替えた際の旧鍵拒否と
|
|
277
|
+
新鍵受理も実HTTPで検証する。同じURLの分散cache invalidationは未証明。
|
|
278
|
+
[再現](validation/registration-remote-red.txt)、
|
|
279
|
+
[DB検証](validation/registration-remote-matrix.txt)。
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
## DCR default_max_age の検証と保存
|
|
283
|
+
|
|
284
|
+
負値が201で受理され、有効値も応答には含まれるが保存されない経路を確認した。CIBA関連の登録/管理更新でのみ、非負の安全な整数であることを検証し、明示した列へ保存する。保存列がなければ拒否する。POSTの不正入力で追加行がなく、PUT失敗で設定と管理用credentialが変わらず、GETでゼロを含む保存値を取得できることを試験した。これはmetadataの整合性修正であり、認証freshness enforcementの実装や、その不備による攻撃成立を主張するものではない。[比較契約](research/registration-reference-contract.md#default_max_age-validation-and-persistence)。
|
|
285
|
+
|
|
286
|
+
## Pairwise sector変更後のJWTの本人対応付け
|
|
287
|
+
|
|
288
|
+
管理更新後の現在のsectorからsubを再計算すると、旧JWTのUserInfoが拒否される。
|
|
289
|
+
nodeでは既存opaque tokenのUserInfoとrefreshが新sectorのsubを返すことを実測した。
|
|
290
|
+
gemは明示migrationで発行時subをtoken行に保持し、署名済みsubとの一致を確認した後、
|
|
291
|
+
保存された内部accountで属性を取得する。UserInfoの公開subだけは現行client設定を使う。
|
|
292
|
+
旧JWTのsubを現行subに差し替えて再署名したfixtureも、保存済み発行subjectとの不一致で拒否する。
|
|
293
|
+
この変更でclient/内部accountのbindingを省略していない。NULLの旧行は従来の検証を維持する。
|
|
294
|
+
[再現](validation/pairwise-sector-update-red.txt)、[回帰](validation/pairwise-sector-update-matrix.txt)。
|