feat(be,fe): cap SSO sessions with session_max_age_seconds - #4187
feat(be,fe): cap SSO sessions with session_max_age_seconds#4187sea-snake wants to merge 4 commits into
Conversation
An organization can now set `session_max_age_seconds` in its `ii-openid-configuration` to say how long a sign-in through its domain stays valid. It defaults to eight hours, is rejected above the 30-day delegation ceiling, and is the only place seconds appear: the parser converts once, so everything downstream is nanoseconds. The deadline is computed at the ceremony, where the discovery cache is warm, and carried on the internal SSO bundle to the point of use, so an evicted cache entry or a refreshed policy can't move a live session's deadline. Attribute certification then stamps it as `sso:<domain>:expires_at_timestamp_ns`, scoped to the domain whose policy set it rather than under `implicit:`, because one bundle can carry attributes from several scopes with different lifetimes and a verifier must enforce the deadline belonging to the scope it reads. The five-minute `SSO_ATTR_BUNDLE_TTL_NS` is untouched: it marks a ceremony as fresh, which is a different question from when the session ends. Enforcement is the relying party's: this only signs the deadline. `mo:identity-attributes` and other verifiers need to reject a bundle past it before the field becomes a control rather than a claim.
The sign-in flow now carries the organization's session length from discovery onto the authenticated session, and the delegation handler bounds the `maxTimeToLive` it requests by it. An app asking for 30 days on an eight-hour domain gets eight hours. This is a UX cap, not a security boundary: it lets the frontend's own delegation-expiry check drive re-authentication instead of needing to know about attribute expiry. The organization's deadline is enforced by the expiry inside the certified attribute bundle.
|
✅ No security or compliance issues detected. Reviewed everything up to dbc4b2f. Security Overview
Detected Code Changes
|
There was a problem hiding this comment.
Pull request overview
Adds organization-configurable SSO session lifetime (session_max_age_seconds) to II’s SSO discovery document, propagates it through the SSO sign-in ceremony into the certified SSO bundle and ICRC-3 attributes, and uses it in the frontend to cap requested delegation TTL for SSO-based sign-ins.
Changes:
- Parse/validate
session_max_age_secondsduring SSO discovery (default 8h; reject 0, overflow, and > 30-day ceiling) and expose it assession_max_age_nsacross the interface. - Compute a fixed
session_expires_at_nsatsso_prepare_delegation, carry it in the internal certified SSO bundle, and stamp a scopedsso:<domain>:expires_at_timestamp_nsclaim when relevant SSO attributes are present. - Frontend threads
sessionMaxAgeNsthrough authenticated state and clamps delegationmaxTimeToLivefor SSO sessions.
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/internet_identity/src/openid/sso.rs | Adds session_max_age_seconds parsing/validation and stores session_max_age_ns in discovered config. |
| src/internet_identity/src/openid/sso_gating.rs | Carries session_max_age_ns through VerifiedSsoLogin for use during the ceremony. |
| src/internet_identity/src/openid/sso_bundle.rs | Extends internal SSO bundle encoding/decoding with session_expires_at_ns. |
| src/internet_identity/src/openid.rs | Exposes session_max_age_ns in get_sso_discovery_status. |
| src/internet_identity/src/main.rs | Computes session deadline at ceremony, includes it in the bundle, and forwards it into attribute certification. |
| src/internet_identity/src/attributes.rs | Conditionally stamps sso:<domain>:expires_at_timestamp_ns into certified pairs when applicable. |
| src/internet_identity/internet_identity.did | Extends SsoDiscovery Candid type with session_max_age_ns. |
| src/internet_identity_interface/src/internet_identity/types.rs | Extends Rust interface type SsoDiscovery with session_max_age_ns. |
| src/frontend/src/lib/utils/ssoDiscovery.ts | Adds sessionMaxAgeNs to discovery result mapping. |
| src/frontend/src/lib/utils/ssoDiscovery.test.ts | Updates discovery mapping test fixture/expectations for sessionMaxAgeNs. |
| src/frontend/src/lib/utils/sessionDuration.ts | Adds cappedMaxTimeToLive helper to clamp delegation TTL by SSO session max age. |
| src/frontend/src/lib/utils/sessionDuration.test.ts | Adds unit tests for cappedMaxTimeToLive. |
| src/frontend/src/lib/stores/channelHandlers/delegation.ts | Applies cappedMaxTimeToLive when preparing account delegations. |
| src/frontend/src/lib/stores/authentication.store.ts | Stores optional ssoSessionMaxAgeNs on authenticated sessions. |
| src/frontend/src/lib/generated/internet_identity_types.d.ts | Regenerated types to include session_max_age_ns (and MCP doc/order adjustments). |
| src/frontend/src/lib/generated/internet_identity_idl.js | Regenerated IDL to include session_max_age_ns (and MCP record field order change). |
| src/frontend/src/lib/flows/authLastUsedFlow.svelte.ts | Carries discovered sessionMaxAgeNs into authenticationStore for last-used SSO flows. |
| src/frontend/src/lib/flows/authFlow.svelte.ts | Carries discovered sessionMaxAgeNs into authenticationStore for standard SSO flows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…from The conversion was called after the hop-2 document validation, which split validation of the hop-1 document across two places. It now happens inside the hop-1 validator, which returns the value, so the fill has one call per document. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both documents were validated in place and then read field by field, so using an unchecked value was a matter of remembering to call the right helper first. Each hop now has one function taking the raw document and returning a validated one: - `validate_ii_config` consumes `IIOpenIdConfiguration` and returns `ValidatedIIOpenIdConfiguration`, absorbing the length bounds, the hop-2 URL check, the `app_clients` parse, the stable-claim default and the session-length conversion. - `validate_discovery_document` consumes `DiscoveryDocument` and returns `ValidatedDiscoveryDocument`, absorbing the length bounds, the host self-assertion checks and the scope defaulting. It takes the validated hop-1 document, so the URL it compares hosts against is itself checked. `validate_discovery_url` hands back the URL it checked rather than `()`, so its three callers bind the checked value. `discovery_fill` is now a fetch and a validate per hop, then assembly. Also renames the bundle's new field to `session_expiry_ns`, since `expiry_ns` and `expires_at_ns` were two names for one kind of value in the same file.
|
Superseded by stack #4191, which splits this into the three layers it was mixing:
Same final tree, verified with an empty diff against this branch before splitting. |
Lets an SSO organization say how long a sign-in through its domain stays valid, and carries that deadline to the two places that can act on it.
Adds
session_max_age_secondsto theii-openid-configurationdocument. It defaults to eight hours, is rejected if zero or above the 30-day delegation ceiling, and is converted to nanoseconds once, in the parser: seconds exist only in that document.How the deadline travels
DiscoveredConfigcarries it assession_max_age_nsin the existing cache, refreshed on the same one-hour TTL.sso_prepare_delegation) reads it offVerifiedSsoLoginand computessession_expires_at_nsonce. This is deliberate: it is the one place with a warm cache, so an evicted entry or a policy refresh can never move a live session's deadline.expiry_nsis untouched, because "this ceremony just happened" is a different question from "the session ends here".sso:<domain>:expires_at_timestamp_nsas aNatwhen the bundle carries any attribute for that domain.maxTimeToLiveit requests by it.Why the key is domain-scoped rather than
implicit:One bundle can carry attributes from several scopes with different lifetimes, since
prepare_icrc3_attributesmatches per spec into one map. A single top-level expiry would have to be the minimum across scopes, which either shortens an unrelated OpenID attribute or misstates the deadline for one of them. A verifier must enforce the deadline belonging to the scope it reads, so the deadline is keyed to the domain whose policy set it.The key can't be requested, only stamped:
AttributeNameacceptsemail,name, andverified_emailonly, so a request for it fails validation, andlist_available_attributesiteratesAttributeName::all()and won't list it.Enforcement, and what this PR does not do
The delegation cap is a UX measure: it lets the frontend's existing expiry check drive re-authentication, and it is applied in the sign-in flow, so it is not a boundary anything relies on. A delegation identifies the identity rather than the SSO session, so any other access method on the anchor can mint a fresh one regardless.
The security property is the bundle expiry, and it is fail-open until verifiers check it. This PR only signs the deadline.
mo:identity-attributesand any hand-rolled verifier need to reject a bundle whosesso:<domain>:expires_at_timestamp_nshas passed, and only once II always stamps it can a missing key be treated as invalid rather than as an older II. Until then the field is a claim, not a control.Testing
cargo test -p internet_identity --lib --bins: 667 pass.cargo clippy -p internet_identity -p internet_identity_interface --all-targets: clean.tsc --project tsconfig.all.json: clean.eslint: clean.findWebAuthnFlows.test.tsandiiConnection.test.tsreproduce onmainunchanged.Docs for the administrator-facing half are in dfinity/developer-docs#340.
🤖 Generated with Claude Code