Skip to content

feat(be,fe): cap SSO sessions with session_max_age_seconds - #4187

Closed
sea-snake wants to merge 4 commits into
mainfrom
feat/sso-session-max-age
Closed

feat(be,fe): cap SSO sessions with session_max_age_seconds#4187
sea-snake wants to merge 4 commits into
mainfrom
feat/sso-session-max-age

Conversation

@sea-snake

Copy link
Copy Markdown
Contributor

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_seconds to the ii-openid-configuration document. 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

  1. Discovery parses and bounds the value, and DiscoveredConfig carries it as session_max_age_ns in the existing cache, refreshed on the same one-hour TTL.
  2. The ceremony (sso_prepare_delegation) reads it off VerifiedSsoLogin and computes session_expires_at_ns once. 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.
  3. The internal SSO bundle gains a fourth length-prefixed field carrying that deadline, so attribute certification never has to re-read the cache. Its own five-minute expiry_ns is untouched, because "this ceremony just happened" is a different question from "the session ends here".
  4. Attribute certification stamps sso:<domain>:expires_at_timestamp_ns as a Nat when the bundle carries any attribute for that domain.
  5. The sign-in flow carries the value from discovery onto the authenticated session, and the delegation handler bounds the maxTimeToLive it 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_attributes matches 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: AttributeName accepts email, name, and verified_email only, so a request for it fails validation, and list_available_attributes iterates AttributeName::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-attributes and any hand-rolled verifier need to reject a bundle whose sso:<domain>:expires_at_timestamp_ns has 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

  • New unit tests for the seconds-to-nanoseconds conversion, including the default, zero, the ceiling boundary, and a value that overflows nanoseconds (rejected rather than saturated).
  • New unit tests for the stamping decision, covering the scoped key, a bundle with no attributes for that domain, two SSO domains in one bundle, and non-SSO sessions.
  • New bundle round-trip and malformed-field coverage for the added field.
  • New frontend tests for the clamp.
  • 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.
  • Integration tests were not run locally. The 18 pre-existing failures in findWebAuthnFlows.test.ts and iiConnection.test.ts reproduce on main unchanged.

Docs for the administrator-facing half are in dfinity/developer-docs#340.

🤖 Generated with Claude Code

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.
Copilot AI lite review requested due to automatic review settings August 6, 2026 12:52
@sea-snake
sea-snake requested a review from a team as a code owner August 6, 2026 12:52
@zeropath-ai

zeropath-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to dbc4b2f.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/frontend/src/lib/flows/authFlow.svelte.ts
    Introduce sessionMaxAgeNs in SSO flow and propagate to sign-in flow
► src/frontend/src/lib/flows/authLastUsedFlow.svelte.ts
    Capture and propagate ssoSessionMaxAgeNs to signed-in flow
Enhancement ► src/frontend/src/lib/generated/internet_identity_idl.js
    Add session_max_age_ns field to SSO discovery result type
Enhancement ► src/frontend/src/lib/generated/internet_identity_types.d.ts
    Add ssoSessionMaxAgeNs to Authenticated type and session_max_age_ns description to SSO fields
Enhancement ► src/frontend/src/lib/stores/authentication.store.ts
    Add ssoSessionMaxAgeNs to Authenticated interface
Enhancement ► src/frontend/src/lib/stores/channelHandlers/delegation.ts
    Use ssoSessionMaxAgeNs to cap maxTimeToLive when delegating
Enhancement ► src/frontend/src/lib/utils/sessionDuration.ts
    Add cappedMaxTimeToLive function to cap delegation TTL by SSO session max age and tests for it
Enhancement ► src/frontend/src/lib/utils/ssoDiscovery.ts
    Extend SSO discovery to include sessionMaxAgeNs and propagate in results
Enhancement ► src/frontend/src/lib/utils/ssoDiscovery.test.ts
    Update tests to reflect session_max_age_ns usage in discovery results
Enhancement ► src/internet_identity/internet_identity.did
    Document session_max_age_ns for SSO discovery
Enhancement ► src/internet_identity/src/openid/sso.rs
    Introduce DEFAULT_SESSION_MAX_AGE_NS and integrate session_max_age_ns handling in config/validation structures
Enhancement ► src/internet_identity/src/openid.rs
    Pass session_expiry_ns into SSO attribute bundle preparation and usage in attribute sharing
Enhancement ► src/internet_identity/src/openid/sso_bundle.rs
    Extend SSO attribute bundle encoding/decoding to include session_expiry_ns; update related structures and tests
Enhancement ► src/internet_identity/src/attributes.rs
    Add sso_session_expiry_entry helper and integrate into certificate attribute handling with tests
Enhancement ► src/internet_identity/src/main.rs
    Propagate session_max_age_ns and session_expiry_ns through openid flow and attribute sharing
Enhancement ► src/internet_identity/src/openid/sso_gating.rs
    (Partial context in diff; updated as part of flow integration)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_seconds during SSO discovery (default 8h; reject 0, overflow, and > 30-day ceiling) and expose it as session_max_age_ns across the interface.
  • Compute a fixed session_expires_at_ns at sso_prepare_delegation, carry it in the internal certified SSO bundle, and stamp a scoped sso:<domain>:expires_at_timestamp_ns claim when relevant SSO attributes are present.
  • Frontend threads sessionMaxAgeNs through authenticated state and clamps delegation maxTimeToLive for 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.

Comment thread src/internet_identity/src/openid/sso_bundle.rs
@sea-snake sea-snake changed the title feat: SSO session_max_age_seconds feat(be,fe): cap SSO sessions with session_max_age_seconds Aug 6, 2026
sea-snake and others added 2 commits August 6, 2026 15:06
…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.
@sea-snake

Copy link
Copy Markdown
Contributor Author

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.

@sea-snake sea-snake closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants