docs(claims-contract): amr and acr cross the boundary, and a refresh carries them - #33
Open
y1o1 wants to merge 1 commit into
Open
docs(claims-contract): amr and acr cross the boundary, and a refresh carries them#33y1o1 wants to merge 1 commit into
y1o1 wants to merge 1 commit into
Conversation
…carries them auth.provider now stamps `amr` and `acr` on access tokens (#481): `amr` from the session's recorded authentication methods on the authorization_code, session and WebAuthn grants, `acr` from a satisfied `acr_values` on authorization_code, and a refresh carries both onto the tokens it mints, since a refresh does not re-authenticate. Only well-formed values are stamped. The verifier has no built-in collector for either; a deployment gating on them maps the verified claim with PayloadClaimAttributeCollector. The row says what the claims mean at the boundary — the original authentication event, as auth_time is — and that absence is not a statement about the user. Lands with the provider pin that carries the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Coordinate the provider pin/release update and address the documented wording corrections.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This documentation-only PR adds the amr/acr claims contract across English and Japanese documentation.
Changes:
- Documents provider stamping, validation, refresh propagation, and verifier mapping.
- Adds authentication-event semantics and a Japanese counterpart.
File summaries
| File | Review summary |
|---|---|
docs/claims-contract.md |
Moderate (3 votes): coordinate with the provider release and E2E pin update. Nits (1 vote each): qualify collector and pre-#573 refresh behavior, and clarify WebAuthn authentication-event semantics. |
docs/claims-contract.ja.md |
Moderate (2 votes): coordinate with the provider release and E2E pin update. Nits (1 vote each): qualify collector and pre-#573 refresh behavior, and clarify WebAuthn authentication-event semantics. |
Review details
Suppressed comments (4)
docs/claims-contract.ja.md:24
PayloadClaimAttributeCollectoris a built-in generic collector and the next sentence explicitly tells deployments to use it, so 「組み込み collector は読まない」 is misleading; qualify it as 「デフォルトでは読まない」 (or 「専用 collector はない」). Also, auth.provider preserves pre-upgrade behavior: refresh tokens minted before #573 carry neither claim through subsequent refreshes, so the unconditional 「どちらも refresh token に載り」 overstates continuity. Please make both qualifications in this row.
| `amr` / `acr` | ユーザーがどう認証したか、どの保証レベルを満たしたか([auth.provider#481](https://github.com/o3co/auth.provider/issues/481))。`amr` はセッションが記録した RFC 8176 のメソッド列 — パスキーなら `["hwk"]` — で、`authorization_code`・`session`・WebAuthn の各 grant が刻印する。`acr` は `/authorize` が `acr_values` から満たした値で、`authorization_code` だけが刻印する。どちらも refresh token に載り、refresh は再認証ではないので、refresh が発行するすべての token に引き継がれる(`oauth/src/grants/refreshToken.mts`)。刻印されるのは形の正しい値だけ: `amr` は空でない文字列の空でない配列、`acr` は空でない文字列(`core/src/grants/authenticationClaims.mts`)。それ以外は省かれ、空で送られることはない。 | 組み込み collector は読まない。これらでゲートするデプロイは、検証済み claim から自前の attribute キーへの `PayloadClaimAttributeCollector` マッピング(`packages/builtins`)を宣言し、Rule を書く。 | **元の**認証イベント。`auth_time` と同じ(OIDC Core §12.2)— refresh された access token が示すのは、それが連なるログインであって refresh ではない。無いことは provider が何も記録しなかったことを意味し、ユーザーが第二要素を使わなかったという表明ではない。 |
docs/claims-contract.ja.md:24
- “セッションが記録した” is not true for the WebAuthn grant: it mints tokens without creating or reading a
UserSessionand stampsamr: ["hwk"]directly. Please describe this as the authentication event’s method list (or distinguish the session-backed and WebAuthn cases), so the Japanese contract does not imply that a passkey grant requires a session.
| `amr` / `acr` | ユーザーがどう認証したか、どの保証レベルを満たしたか([auth.provider#481](https://github.com/o3co/auth.provider/issues/481))。`amr` はセッションが記録した RFC 8176 のメソッド列 — パスキーなら `["hwk"]` — で、`authorization_code`・`session`・WebAuthn の各 grant が刻印する。`acr` は `/authorize` が `acr_values` から満たした値で、`authorization_code` だけが刻印する。どちらも refresh token に載り、refresh は再認証ではないので、refresh が発行するすべての token に引き継がれる(`oauth/src/grants/refreshToken.mts`)。刻印されるのは形の正しい値だけ: `amr` は空でない文字列の空でない配列、`acr` は空でない文字列(`core/src/grants/authenticationClaims.mts`)。それ以外は省かれ、空で送られることはない。 | 組み込み collector は読まない。これらでゲートするデプロイは、検証済み claim から自前の attribute キーへの `PayloadClaimAttributeCollector` マッピング(`packages/builtins`)を宣言し、Rule を書く。 | **元の**認証イベント。`auth_time` と同じ(OIDC Core §12.2)— refresh された access token が示すのは、それが連なるログインであって refresh ではない。無いことは provider が何も記録しなかったことを意味し、ユーザーが第二要素を使わなかったという表明ではない。 |
docs/claims-contract.md:24
PayloadClaimAttributeCollectoris a built-in generic collector and the next sentence explicitly tells deployments to use it, so “No built-in collector reads them” is misleading; qualify it as “by default” (or “no dedicated collector”). Also, auth.provider preserves pre-upgrade behavior: refresh tokens minted before #573 carry neither claim through subsequent refreshes, so the unconditional “Both travel on the refresh token” overstates continuity. Please make both qualifications in this row.
| `amr` / `acr` | How, and to which assurance level, the user authenticated ([auth.provider#481](https://github.com/o3co/auth.provider/issues/481)). `amr` is the RFC 8176 method list the session recorded — `["hwk"]` for a passkey — stamped by the `authorization_code`, `session` and WebAuthn grants; `acr` is the value `/authorize` satisfied from `acr_values`, stamped by `authorization_code` only. Both travel on the refresh token and are carried onto every token a refresh mints, since a refresh does not re-authenticate (`oauth/src/grants/refreshToken.mts`). Only well-formed values are stamped: `amr` a non-empty array of non-empty strings, `acr` a non-empty string (`core/src/grants/authenticationClaims.mts`); anything else is omitted, never sent empty. | No built-in collector reads them. A deployment that gates on them declares a `PayloadClaimAttributeCollector` mapping (`packages/builtins`) from the verified claim to an attribute key of its own, and writes the rule. | The **original** authentication event, as `auth_time` is (OIDC Core §12.2) — a refreshed access token reports the login it descends from, not the refresh. Absent means the provider recorded none; it is not a statement that the user did not use a second factor. |
docs/claims-contract.md:24
- “the session recorded” is not true for the WebAuthn grant: that grant mints tokens without creating or reading a
UserSessionand stampsamr: ["hwk"]directly. Please describe this as the authentication event’s method list (or split the session-backed and WebAuthn cases), otherwise readers may infer that passkey grant tokens require a session.
| `amr` / `acr` | How, and to which assurance level, the user authenticated ([auth.provider#481](https://github.com/o3co/auth.provider/issues/481)). `amr` is the RFC 8176 method list the session recorded — `["hwk"]` for a passkey — stamped by the `authorization_code`, `session` and WebAuthn grants; `acr` is the value `/authorize` satisfied from `acr_values`, stamped by `authorization_code` only. Both travel on the refresh token and are carried onto every token a refresh mints, since a refresh does not re-authenticate (`oauth/src/grants/refreshToken.mts`). Only well-formed values are stamped: `amr` a non-empty array of non-empty strings, `acr` a non-empty string (`core/src/grants/authenticationClaims.mts`); anything else is omitted, never sent empty. | No built-in collector reads them. A deployment that gates on them declares a `PayloadClaimAttributeCollector` mapping (`packages/builtins`) from the verified claim to an attribute key of its own, and writes the rule. | The **original** authentication event, as `auth_time` is (OIDC Core §12.2) — a refreshed access token reports the login it descends from, not the refresh. Absent means the provider recorded none; it is not a statement that the user did not use a second factor. |
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| | `iss` | デプロイ設定の issuer。必須、リクエスト由来にしない。 | jose 検証でピン — `oauth.jwt.issuer` / `OAUTH_JWT_ISSUER`。 | デプロイの identity。 | | ||
| | `typ`(header) | access token は `at+jwt`(RFC 9068)、refresh token は `rt+jwt`、ID token(wire 名 `id_token`)は auth.provider v0.10.0 以降は標準の `JWT`(それ以前は非標準の `id+jwt` で、`typ` を厳格に検証する外部 RP に弾かれていた)。auth.provider#402(2026-08-30)で移行期間は終了し、現在は `JWT` のみを受理する。 | `oauth.jwt.tokenType`、default `at+jwt`。比較時に `application/` prefix は無視。 | 3 種の token を区別する **唯一の** 判別子 — このピンが、同じ鍵で署名された refresh / id token が `/verify` を通ることを防いでいる。防いでいる実体は **`at+jwt` と互いに素であること** であり、`JWT` は `id+jwt` と同様にこれを満たす。claim レベルの代替チェックは存在しない。 | | ||
| | `exp` / `iat` | 常に刻印(`core/src/grants/token.mts`)。 | 両方必須(`tokenAuthenticator` の必須 claim チェック + 常設の `maxTokenAgeSeconds` 上限)。`exp` のない token は永続 credential として拒否。 | 寿命。verifier は issuer の規律を信頼せず、issuer の `exp` をさらに上限で抑える。 | | ||
| | `amr` / `acr` | ユーザーがどう認証したか、どの保証レベルを満たしたか([auth.provider#481](https://github.com/o3co/auth.provider/issues/481))。`amr` はセッションが記録した RFC 8176 のメソッド列 — パスキーなら `["hwk"]` — で、`authorization_code`・`session`・WebAuthn の各 grant が刻印する。`acr` は `/authorize` が `acr_values` から満たした値で、`authorization_code` だけが刻印する。どちらも refresh token に載り、refresh は再認証ではないので、refresh が発行するすべての token に引き継がれる(`oauth/src/grants/refreshToken.mts`)。刻印されるのは形の正しい値だけ: `amr` は空でない文字列の空でない配列、`acr` は空でない文字列(`core/src/grants/authenticationClaims.mts`)。それ以外は省かれ、空で送られることはない。 | 組み込み collector は読まない。これらでゲートするデプロイは、検証済み claim から自前の attribute キーへの `PayloadClaimAttributeCollector` マッピング(`packages/builtins`)を宣言し、Rule を書く。 | **元の**認証イベント。`auth_time` と同じ(OIDC Core §12.2)— refresh された access token が示すのは、それが連なるログインであって refresh ではない。無いことは provider が何も記録しなかったことを意味し、ユーザーが第二要素を使わなかったという表明ではない。 | |
| | `iss` | Deployment-configured issuer; required, never request-derived. | Pinned by jose verification — `oauth.jwt.issuer` / `OAUTH_JWT_ISSUER`. | Deployment identity. | | ||
| | `typ` (header) | `at+jwt` on access tokens (RFC 9068), `rt+jwt` on refresh tokens, and the standard `JWT` on ID tokens (wire name `id_token`) since auth.provider v0.10.0 — it was the nonstandard `id+jwt` before, which strict external RPs rejected; auth.provider#402 (2026-08-30) closed the dual-accept window, so only `JWT` is accepted now. | `oauth.jwt.tokenType`, default `at+jwt`; an `application/` prefix on either side is ignored when comparing. | The **only** discriminator between the three token kinds — the pin is what keeps a refresh or id token signed with the same key from passing `/verify`. What does the keeping-out is being **disjoint from `at+jwt`**, which `JWT` satisfies exactly as `id+jwt` did. There is no claim-level fallback check. | | ||
| | `exp` / `iat` | Always stamped (`core/src/grants/token.mts`). | Both required (`tokenAuthenticator`'s required-claims check + the always-set `maxTokenAgeSeconds` bound); a token without `exp` is refused as a permanent credential. | Lifetime. The verifier additionally caps the issuer's `exp` rather than trusting issuer discipline. | | ||
| | `amr` / `acr` | How, and to which assurance level, the user authenticated ([auth.provider#481](https://github.com/o3co/auth.provider/issues/481)). `amr` is the RFC 8176 method list the session recorded — `["hwk"]` for a passkey — stamped by the `authorization_code`, `session` and WebAuthn grants; `acr` is the value `/authorize` satisfied from `acr_values`, stamped by `authorization_code` only. Both travel on the refresh token and are carried onto every token a refresh mints, since a refresh does not re-authenticate (`oauth/src/grants/refreshToken.mts`). Only well-formed values are stamped: `amr` a non-empty array of non-empty strings, `acr` a non-empty string (`core/src/grants/authenticationClaims.mts`); anything else is omitted, never sent empty. | No built-in collector reads them. A deployment that gates on them declares a `PayloadClaimAttributeCollector` mapping (`packages/builtins`) from the verified claim to an attribute key of its own, and writes the rule. | The **original** authentication event, as `auth_time` is (OIDC Core §12.2) — a refreshed access token reports the login it descends from, not the refresh. Absent means the provider recorded none; it is not a statement that the user did not use a second factor. | |
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A new row in
docs/claims-contract.md(and.ja.md) foramr/acr, which auth.provider now stamps on access tokens (auth.provider#481, refresh continuity in auth.provider#573):amr— the session's RFC 8176 method list (["hwk"]for a passkey) — on theauthorization_code,sessionand WebAuthn grants;acr— the value/authorizesatisfied fromacr_values— onauthorization_code. Both ride the refresh token and are carried onto every token a refresh mints. Only well-formed values are stamped (core/src/grants/authenticationClaims.mts).PayloadClaimAttributeCollectormapping to its own attribute key and writes the rule.auth_timeis (OIDC Core §12.2); absence means the provider recorded none, not that the user used no second factor.Per the document's change protocol, this row belongs with the E2E pin that carries the provider change: merge with (or after) the release-cut pin bump to auth.provider's next version.
Test plan
amr/acrtoday; the provider's own suites pin the behaviour (authorization.test.mts,refreshToken.test.mts,sessionGrant.security.test.mts,grant.refreshToken.test.mts).🤖 Generated with Claude Code