rodauth-ciba 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +4 -4
- data/docs/api.md +8 -5
- data/docs/authentication-claims.md +11 -1
- data/docs/ciba-conformance.md +58 -0
- data/docs/ciba-core-requirements.json +3537 -0
- data/docs/claims.md +5 -1
- data/docs/max-age.md +3 -1
- data/docs/operations.md +28 -1
- data/docs/pairwise.md +20 -4
- data/docs/refresh-tokens.md +32 -0
- data/docs/release-readiness.md +22 -3
- data/docs/release-validation.md +5 -0
- data/docs/requirements-audit-current.json +2 -1
- data/docs/requirements-audit-current.md +2 -0
- data/examples/extensions_smoke.rb +18 -1
- data/lib/rodauth/ciba/pairwise.rb +7 -3
- data/lib/rodauth/ciba/registration.rb +4 -0
- data/lib/rodauth/ciba/version.rb +1 -1
- data/lib/rodauth/features/ciba.rb +195 -19
- metadata +4 -2
data/docs/claims.md
CHANGED
|
@@ -37,7 +37,11 @@ Requested JSON and consent JSON are stored separately. Snapshot fields are froze
|
|
|
37
37
|
- Rejected/unselected attributes are not requested from the account callback. Poll parameters cannot expand the stored request. Client-supplied `value` never becomes identity data.
|
|
38
38
|
- The compiled selection is persisted on the issued OAuth row, separately from the request. UserInfo binds to the exact presented opaque token or a signed JWT's `urn:rodauth:ciba:token_id` reference, with account/client checks and DB revocation checks. The internal reference is omitted from ID Tokens. It is not authorization by itself.
|
|
39
39
|
|
|
40
|
-
The
|
|
40
|
+
The gem validates essential as a Boolean and values as an array, checks standard claim value types, and preserves qualifiers with each issued claim projection. It compares value/values to trusted output values and omits mismatching ordinary claims for both response targets. It never uses requested values as account data. Missing ordinary essential claims do not cause errors, as required by OIDC Core5.5.1. Voluntary ACR follows its specific rule and may return the achieved ACR rather than a preferred value.
|
|
41
|
+
|
|
42
|
+
Requested sub must match the actual RP-facing public/pairwise subject. Essential ID Token acr with value/values requires a supplied matching achieved ACR. Approval rejects these mismatches before committing; issuance and refresh recheck saved requirements. Required auth_time cannot be missing or omitted to satisfy a conflicting qualifier. Mandatory ID Token fields, including a supplied nonce, cannot be removed to satisfy qualifiers. Signed UserInfo issuer/audience constraints are checked against upstream's final values; a later incompatible issuer change fails UserInfo with invalid_token. Pairwise UserInfo uses the issued subject after sector changes so it remains consistent with the ID Token.
|
|
43
|
+
|
|
44
|
+
The application must perform actual authentication, select an achieved ACR, supply truthful authentication context, enforce freshness and business policy, and acquire trusted claim values. Localized, aggregated and distributed claim processing is not implemented. Custom claim semantic types remain application-owned. These limits prevent claiming complete OIDC claims parity.
|
|
41
45
|
|
|
42
46
|
Legacy JWTs without an exact issuance reference never inherit explicit claims from another OAuth row for the same account/client. They retain the upstream scope-based path; this does not retroactively add per-row revocation to those JWTs. In particular, removing a legacy issuance row is not permission to reuse a newer row's explicit consent. This migration boundary has a regression test using an actual pre-capability JWT.
|
|
43
47
|
|
data/docs/max-age.md
CHANGED
|
@@ -22,7 +22,9 @@ from no policy. This is an internal representation adaptation, not an HTTP respo
|
|
|
22
22
|
change. Client metadata changes after acceptance do not alter the saved policy.
|
|
23
23
|
Applications must enforce freshness during authentication; approval does not reject
|
|
24
24
|
an old supplied auth_time automatically. Selected max_age causes auth_time to be
|
|
25
|
-
selected for ID Token output
|
|
25
|
+
selected and required for ID Token output. Approval without a verified time is
|
|
26
|
+
rejected, and issuance/refresh reject missing required context without consuming
|
|
27
|
+
the source. This does not invent a time or automatically enforce freshness.
|
|
26
28
|
Refresh preserves the original authentication claim selection/context and does
|
|
27
29
|
not start a new authentication request or reapply current client defaults.
|
|
28
30
|
|
data/docs/operations.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Deployment and operating boundaries
|
|
2
2
|
|
|
3
|
+
## Upgrading the Core conformance boundaries
|
|
4
|
+
|
|
5
|
+
Claim qualifiers now form an enforced backend contract. Old pending/refresh
|
|
6
|
+
sources with incompatible sub, essential ACR or malformed options fail closed;
|
|
7
|
+
supply valid authentication context or expire/drain those sources. Newly issued
|
|
8
|
+
claim projections retain value/values filters. Existing issued projections have
|
|
9
|
+
no qualifier history and cannot be upgraded by guessing which request produced
|
|
10
|
+
them: drain or expire affected old tokens before relying on these guarantees.
|
|
11
|
+
Upgrade every worker together; older workers ignore the new projection members.
|
|
12
|
+
Pairwise UserInfo now returns the subject stored at issuance, including after
|
|
13
|
+
sector configuration changes. This corrects the earlier recomputation behavior.
|
|
14
|
+
|
|
15
|
+
Request expiry now starts at backchannel-handler entry rather than after hint
|
|
16
|
+
resolution. Slow client authentication/resolvers/hooks/AD dispatch consume that
|
|
17
|
+
budget; enqueue AD work promptly. An acknowledgement can arrive after expiry if
|
|
18
|
+
dispatch is slow; polling then returns expired_token. The deadline is not extended
|
|
19
|
+
by processing. Already committed requests remain for bounded cleanup.
|
|
20
|
+
|
|
21
|
+
New requests save auth_time_required for selected max_age or essential ID Token
|
|
22
|
+
auth_time, in addition to client require_auth_time=true. No schema migration is
|
|
23
|
+
needed. Existing requests/refresh sources retain their old saved selection; the
|
|
24
|
+
upgrade does not infer or fabricate missing authentication evidence. Drain or
|
|
25
|
+
invalidate affected old work before advertising this stricter contract, update
|
|
26
|
+
all workers together, and ensure approval callbacks provide actual auth_time.
|
|
27
|
+
The application still enforces max_age freshness and other essential constraints.
|
|
28
|
+
See [Core conformance boundaries and acceptance gates](ciba-conformance.md).
|
|
29
|
+
|
|
3
30
|
Optional explicit claims require an additional schema migration and coordinated activation; see [claims operations](claims.md). The default base schema and default-disabled behavior do not require those columns.
|
|
4
31
|
|
|
5
32
|
The same activation boundary applies to [resources](resources.md) and [RAR](authorization-details.md). Issuance checks both the original request and its saved consent: if either records a capability whose flag is now disabled, collection returns invalid_grant without consuming the request or issuing a token. This includes old requests bound to new-format consent after migration and explicitly stored empty permission values. Legacy request/consent pairs whose new fields are both null retain baseline behavior. Do not use feature deactivation as a way to erase approval restrictions; drain or explicitly resolve affected work before changing capabilities.
|
|
@@ -71,7 +98,7 @@ Limit unsolicited requests in your application. Optional [user_code](user-code.m
|
|
|
71
98
|
|
|
72
99
|
## Scope and compatibility
|
|
73
100
|
|
|
74
|
-
0.1
|
|
101
|
+
Version 0.2.1 targets poll with login_hint, public subjects and statically provisioned CIBA clients. It delegates confidential client authentication to enabled upstream mechanisms, preserves shared Discovery metadata, and issues signed ID Tokens using upstream's JWT implementation. Basic/post and private_key_jwt/client_secret_jwt have integration coverage; configured JWT algorithms, DPoP and optional mTLS have additional integration evidence in the [alignment status](alignment-closeout.md), including installed real-TLS examples. Production proxy trust remains application-owned. Unsupported features are not silently treated as implemented. Read the gemspec for narrow initial dependency bounds and `docs/release-validation.md` in the repository for actual tested environments. Follow the [0.2.1 upgrade notes](release-readiness.md#upgrade-to-021) for refresh-token reauthentication and coordinated worker updates.
|
|
75
102
|
|
|
76
103
|
No OpenID certification, full Conformance Suite pass, security audit, or blanket support for all Sequel adapters is claimed. Existing authorization-code behavior has a regression test, not the complete upstream test suite. Consumers should use the included protocol mapping as a starting point for application-specific interoperability tests.
|
|
77
104
|
|
data/docs/pairwise.md
CHANGED
|
@@ -42,12 +42,23 @@ retain the canonical account ID. UserInfo must return the same pairwise subject
|
|
|
42
42
|
as ID Token. With pairwise enabled, new CIBA issuances snapshot their subject in that token
|
|
43
43
|
row. JWT UserInfo checks the signed subject against this snapshot and the bound
|
|
44
44
|
client before using the internal account for lookup. After a management sector
|
|
45
|
-
change, old tokens can therefore authenticate while UserInfo
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
change, old tokens can therefore authenticate while UserInfo retains their
|
|
46
|
+
issuance subject, even if the client changes subject_type to public and the
|
|
47
|
+
optional claims capability is disabled. Refresh chains retain the original ID Token subject using
|
|
48
|
+
versioned authentication context, deliberately differing from the previously
|
|
49
|
+
measured node current-sector behavior. Already-issued JWT bytes remain unchanged. Older rows with no
|
|
48
50
|
snapshot retain the prior current-subject check; a sector change can invalidate
|
|
49
51
|
those old JWTs. The migration does not invent historical subjects.
|
|
50
52
|
|
|
53
|
+
Each token request snapshots client metadata immediately after successful client
|
|
54
|
+
authentication. Initial and refreshed tokens use that snapshot for identity,
|
|
55
|
+
signing and encryption recipients even if DCR commits before issuance validation
|
|
56
|
+
or during its hooks. Reloading the client for current eligibility and permission
|
|
57
|
+
checks does not replace the authenticated snapshot. The next HTTP request must
|
|
58
|
+
authenticate against the new registration and uses its new metadata. This binds
|
|
59
|
+
the pairwise sector to the JWKS URI used for proof of key possession; a prior
|
|
60
|
+
key cannot authenticate a new sector merely by racing a registration update.
|
|
61
|
+
|
|
51
62
|
For id_token_hint, configure the existing `resolve_ciba_id_token_subject` callback
|
|
52
63
|
to recover the canonical account in the authenticated client's context. The gem
|
|
53
64
|
first verifies the ID Token's issuer, audience and signature; the hint then starts
|
|
@@ -67,10 +78,15 @@ sector update; subsequent approved issuance uses the new sector subject. The
|
|
|
67
78
|
installed-gem TLS fixture now covers hybrid DCR, real remote-key authentication,
|
|
68
79
|
approval, verified ID Token, saved subject and matching UserInfo. Concurrent
|
|
69
80
|
sector management/issuance tests now verify one issuance subject across storage
|
|
70
|
-
and tokens
|
|
81
|
+
and tokens. UserInfo now retains the subject stored with the presented issuance
|
|
82
|
+
after sector changes; the earlier current-sector expectation is superseded.
|
|
83
|
+
The updated expectation is locally tested on SQLite; the historical three-DB
|
|
84
|
+
result predates this correction. PostgreSQL and
|
|
71
85
|
MySQL force the update to commit while issuance is paused; SQLite tests writer
|
|
72
86
|
contention and either serial order. Concurrent refresh/key changes, arbitrary
|
|
73
87
|
application hooks and additional deployment combinations remain unverified.
|
|
88
|
+
Legacy refresh sources without original ID Token context fail closed; see the
|
|
89
|
+
[refresh migration](refresh-tokens.md#original-id-token-binding-and-migration).
|
|
74
90
|
This is not a completed pairwise parity or release-readiness claim.
|
|
75
91
|
|
|
76
92
|
[Reference contract](research/pairwise-reference-contract.md).
|
data/docs/refresh-tokens.md
CHANGED
|
@@ -57,6 +57,38 @@ reference behavior and the remaining tests. Current authentication context outpu
|
|
|
57
57
|
follows the gem's CIBA ID Token behavior; it is not identical to node's optional
|
|
58
58
|
claim masking.
|
|
59
59
|
|
|
60
|
+
## Original ID Token binding and migration
|
|
61
|
+
|
|
62
|
+
New refresh sources use the existing `authentication_claims` TEXT column as a
|
|
63
|
+
versioned JSON object: `version: 2`, a `selected` string array, and
|
|
64
|
+
`initial_id_token` containing the actual initial `iss`, `sub`, `aud` and, when
|
|
65
|
+
returned, `auth_time`. The values are captured from the final payload passed to
|
|
66
|
+
signing/encryption, including encrypted ID Tokens. Request rows retain their
|
|
67
|
+
previous selection-array format. Rotation copies the original object unchanged.
|
|
68
|
+
No new database column or automatic data migration is required.
|
|
69
|
+
|
|
70
|
+
[OIDC Core 12.2](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse)
|
|
71
|
+
requires refreshed ID Tokens to retain the original issuer, subject and audience.
|
|
72
|
+
Refresh preserves those values and the original disclosed authentication time;
|
|
73
|
+
`iat` is the new issuance time. Refresh ID Tokens omit `nonce` as recommended by
|
|
74
|
+
that section. Changing a pairwise sector does not reinterpret an existing chain:
|
|
75
|
+
its ID Token, access-token subject and UserInfo retain the original subject.
|
|
76
|
+
New CIBA authentications use the current sector. Changed issuer/client ID, or an
|
|
77
|
+
incompatible current public-subject mapping, causes `invalid_grant`.
|
|
78
|
+
|
|
79
|
+
Legacy nil/array JSON and unknown, malformed or incomplete objects are rejected
|
|
80
|
+
with `invalid_grant` before refresh hooks, lock-version mutation, rotation or
|
|
81
|
+
token creation. They are not backfilled from current settings or unrelated access
|
|
82
|
+
rows. This applies to public as well as pairwise refresh tokens. Hook-side changes
|
|
83
|
+
are revalidated after reload inside the transaction. A denied renewal alone does
|
|
84
|
+
not revoke otherwise live access tokens; users need a new CIBA authentication.
|
|
85
|
+
|
|
86
|
+
This changes the stored JSON and the `ciba_current_refresh` snapshot contract.
|
|
87
|
+
Upgrade all workers together; old versions cannot safely process the new object.
|
|
88
|
+
Drain/expire old refresh sources or require reauthentication, and account for
|
|
89
|
+
this in rollback planning. Restoring an old implementation does not preserve
|
|
90
|
+
the new binding guarantee. Do not rewrite live tokens automatically.
|
|
91
|
+
|
|
60
92
|
## Refresh-token revocation
|
|
61
93
|
|
|
62
94
|
Enable upstream `:oauth_token_revocation` to expose its advertised endpoint.
|
data/docs/release-readiness.md
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
# 0.1
|
|
1
|
+
# 0.2.1 release readiness
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Version 0.2.1 incorporates the unpublished 0.2.0 candidate and the documentation
|
|
4
|
+
corrections in PR #3. The existing `v0.2.0` tag is retained; no 0.2.0 gem was
|
|
5
|
+
published as of 2026-10-22. A source version or tag does not establish registry
|
|
6
|
+
publication. Check [RubyGems](https://rubygems.org/gems/rodauth-ciba/versions) for
|
|
7
|
+
published versions and the [changelog](../CHANGELOG.md) for changes.
|
|
8
|
+
|
|
9
|
+
Version 0.1.0 was published on 2026-10-11 from commit
|
|
10
|
+
`74ccc85e4f7af9596fec2b965bd43eed0ff708bc`. Version 0.2.1 contains the
|
|
11
|
+
CIBA Core validation and authenticated-client metadata fixes in PR #2.
|
|
12
|
+
|
|
13
|
+
## Upgrade to 0.2.1
|
|
14
|
+
|
|
15
|
+
Old refresh tokens without the versioned identity context are rejected and require
|
|
16
|
+
reauthentication. No automatic data migration or new database column is provided.
|
|
17
|
+
Stop old workers before starting new workers: mixed 0.1.0/0.2.1 workers are not
|
|
18
|
+
supported. Refresh tokens, pairwise subjects and dynamic registration remain
|
|
19
|
+
experimental and disabled by default.
|
|
20
|
+
|
|
21
|
+
The official OpenID Conformance Suite has not been executed. Local tests and
|
|
22
|
+
hosted CI do not establish certification or full CIBA conformance. The evidence
|
|
23
|
+
below is historical 0.1.0 evidence unless a different revision is stated.
|
|
5
24
|
|
|
6
25
|
## Scope
|
|
7
26
|
|
data/docs/release-validation.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# 0.1.0 release validation
|
|
2
2
|
|
|
3
|
+
This is the historical 0.1.0 validation record. For the current 0.2.1 version's
|
|
4
|
+
status, upgrade requirements and verification limits, see
|
|
5
|
+
[release readiness](release-readiness.md). The versions and artifact names below
|
|
6
|
+
identify the original test runs and have not been relabeled.
|
|
7
|
+
|
|
3
8
|
Edwards client authentication follow-up: Ruby3.3/3.4 full SQLite suites each pass
|
|
4
9
|
334 tests /4,316 assertions with2 row-lock skips
|
|
5
10
|
([log](validation/client-auth-edwards-rubies.txt)). Remote-key/ordinary-code-grant
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
Historical 2026-09-30 audit overlay. For the current local Core final changes and remaining acceptance gates, see [conformance boundaries](ciba-conformance.md) and [requirement ledger](ciba-core-requirements.json). The older partial timing finding below is historical.
|
|
2
|
+
|
|
1
3
|
# 現行構成の要件再監査
|
|
2
4
|
|
|
3
5
|
2026-09-30。初期の159項目カタログを保持し、現在の任意機能に対する再監査を追記する。**159項目を再分類、未分類は0項目**。この分割は最終目標や監査範囲を縮小しない。CIBA以外のRFCやnode固有の挙動は[整合化計画](alignment-roadmap.md)で別途追跡する。
|
|
@@ -167,6 +167,10 @@ require_relative "demo_app"
|
|
|
167
167
|
|
|
168
168
|
refresh = tokens.fetch("refresh_token")
|
|
169
169
|
source = database[:ciba_refresh_tokens].first
|
|
170
|
+
stored_context = JSON.parse(source.fetch(:authentication_claims))
|
|
171
|
+
unless stored_context["version"] == 2 && stored_context["initial_id_token"] == identity.slice("iss", "sub", "aud", "auth_time")
|
|
172
|
+
raise "initial ID Token context was not persisted"
|
|
173
|
+
end
|
|
170
174
|
raise "raw refresh persisted" if JSON.generate(database[:ciba_refresh_tokens].all).include?(refresh)
|
|
171
175
|
raise "wrong refresh digest" unless source[:token_digest] == Digest::SHA256.hexdigest(refresh)
|
|
172
176
|
now = Time.now.to_i
|
|
@@ -180,10 +184,23 @@ require_relative "demo_app"
|
|
|
180
184
|
raise "lost RAR permission" unless renewed["authorization_details"] == expected_details
|
|
181
185
|
renewed_identity, = JWT.decode(renewed.fetch("id_token"), nil, true, jwks: keys, algorithms: ["RS256"],
|
|
182
186
|
verify_iss: true, iss: issuer, verify_aud: true, aud: "demo-client")
|
|
183
|
-
unless renewed_identity["auth_time"] == 1 && renewed_identity
|
|
187
|
+
unless renewed_identity["auth_time"] == 1 && !renewed_identity.key?("nonce") &&
|
|
184
188
|
renewed_identity["email"] == "customer@example.test" && !renewed_identity.key?("email_verified")
|
|
185
189
|
raise "refresh changed authenticated identity or permission"
|
|
186
190
|
end
|
|
191
|
+
successor_ds = database[:ciba_refresh_tokens].where(token_digest: Digest::SHA256.hexdigest(renewed.fetch("refresh_token")))
|
|
192
|
+
successor_source = successor_ds.first
|
|
193
|
+
raise "rotation changed initial context" unless JSON.parse(successor_source[:authentication_claims]) == stored_context
|
|
194
|
+
successor_ds.update(authentication_claims: "[]")
|
|
195
|
+
before_legacy = [database[:ciba_refresh_tokens].all, database[:oauth_grants].all, database[:ciba_grants].all]
|
|
196
|
+
legacy = http.post(metadata.fetch("token_endpoint"), headers.merge(params: {
|
|
197
|
+
grant_type: "refresh_token", refresh_token: renewed.fetch("refresh_token"), resource: resource
|
|
198
|
+
}))
|
|
199
|
+
unless legacy.status == 400 && JSON.parse(legacy.body)["error"] == "invalid_grant" &&
|
|
200
|
+
before_legacy == [database[:ciba_refresh_tokens].all, database[:oauth_grants].all, database[:ciba_grants].all]
|
|
201
|
+
raise "legacy refresh binding failed closed"
|
|
202
|
+
end
|
|
203
|
+
successor_ds.update(authentication_claims: successor_source[:authentication_claims])
|
|
187
204
|
replay = http.post(metadata.fetch("token_endpoint"), headers.merge(params: token_params))
|
|
188
205
|
raise "replay accepted" unless replay.status == 400 && JSON.parse(replay.body)["error"] == "invalid_grant"
|
|
189
206
|
raise "saved token not revoked" unless database[:oauth_grants].get(:revoked_at)
|
|
@@ -20,9 +20,13 @@ module Rodauth
|
|
|
20
20
|
|
|
21
21
|
def fill_with_account_claims(claims, account, scopes, locales)
|
|
22
22
|
result = super
|
|
23
|
-
if @ciba_userinfo_grant_id && request.path == userinfo_path && oauth_application
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if @ciba_userinfo_grant_id && request.path == userinfo_path && oauth_application
|
|
24
|
+
issued = db[oauth_grants_table].where(oauth_grants_id_column => @ciba_userinfo_grant_id).first
|
|
25
|
+
if issued && issued[ciba_token_subject_column]
|
|
26
|
+
claims["sub"] = issued[ciba_token_subject_column]
|
|
27
|
+
elsif (oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type) == "pairwise"
|
|
28
|
+
claims["sub"] = jwt_subject(account[account_id_column], oauth_application)
|
|
29
|
+
end
|
|
26
30
|
end
|
|
27
31
|
result
|
|
28
32
|
end
|
|
@@ -317,6 +317,10 @@ module Rodauth
|
|
|
317
317
|
@oauth_application_params = reset.merge(@oauth_application_params)
|
|
318
318
|
end
|
|
319
319
|
candidate = (existing || {}).merge(@oauth_application_params)
|
|
320
|
+
jwks_uri = candidate[oauth_applications_jwks_uri_column]
|
|
321
|
+
if jwks_uri && !ciba_https_uri?(jwks_uri)
|
|
322
|
+
register_throw_json_response_error("invalid_client_metadata", "CIBA client jwks_uri must use HTTPS")
|
|
323
|
+
end
|
|
320
324
|
if !encryption.empty? &&
|
|
321
325
|
!IdTokenEncryption::SYMMETRIC_ALGORITHMS.include?(candidate[oauth_applications_id_token_encrypted_response_alg_column]) &&
|
|
322
326
|
!candidate[oauth_applications_jwks_column] && !candidate[oauth_applications_jwks_uri_column]
|