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.
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 application remains responsible for evaluating essential/value/values, acr, and localized claim requirements before approving. The stored request preserves those objects for that purpose; default attribute acquisition returns trusted account values rather than asserting that requested constraints were met. Aggregated/distributed claim processing is not implemented by this capability. These limits prevent claiming complete OIDC claims parity.
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, but does not invent an authentication time.
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.0 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.
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 emits the new
46
- sector subject, as measured with node opaque tokens. Refresh also uses the new
47
- sector, and already-issued JWT bytes remain unchanged. Older rows with no
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, followed by current-sector UserInfo, on all three DBs. PostgreSQL and
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).
@@ -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.
@@ -1,7 +1,26 @@
1
- # 0.1.0 release readiness
1
+ # 0.2.1 release readiness
2
2
 
3
- RubyGems publication is pending. This document separates the supported release
4
- scope from historical development notes and opt-in experiments.
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
 
@@ -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
@@ -3217,5 +3217,6 @@
3217
3217
  "execution": null
3218
3218
  }
3219
3219
  ],
3220
- "pending_ids": []
3220
+ "pending_ids": [],
3221
+ "superseded_for_current_changes_by": "ciba-core-requirements.json"
3221
3222
  }
@@ -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["nonce"] == "installed-extension-check" &&
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
- (oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type) == "pairwise"
25
- claims["sub"] = jwt_subject(account[account_id_column], oauth_application)
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]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Rodauth
3
3
  module CibaSupport
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
6
6
  end