Skip to content

feat(be,fe): address SSO review feedback on #3785 (follow-up)#3803

Merged
aterga merged 6 commits intodfinity:mainfrom
aterga:sso-review-fixes-3785
Apr 24, 2026
Merged

feat(be,fe): address SSO review feedback on #3785 (follow-up)#3803
aterga merged 6 commits intodfinity:mainfrom
aterga:sso-review-fixes-3785

Conversation

@aterga
Copy link
Copy Markdown
Collaborator

@aterga aterga commented Apr 24, 2026

Follow-up to #3785 addressing @sea-snake's review. All five unresolved threads there are addressed here, and will be marked resolved on #3785 with pointers back to this PR.

Stacked on top of #3785: this branch includes all commits from #3785 plus three new commits that cover the review fixes.

Changes

1. Sign in with SSOContinue with SSO

SSO button aria-label in PickAuthenticationMethod and AddAccessMethod, and the h1 inside the SSO screen, now read "Continue with SSO" — the downstream flow can end in either a sign-in or a sign-up, so "Sign in" was misleading.

2. Slim mapSubmitError to user-actionable copy

SignInWithSso.svelte#mapSubmitError used to enumerate every shape of provider misconfiguration (hostname mismatch, non-HTTPS endpoints, malformed discovery document, ...). Those read as implementation-detail leakage. Keep only the branches the user or their SSO admin can act on — domain not configured, canary allowlist, OAuth provider errors from the callback fragment, rate limits — and fall back to a generic "SSO sign-in for <domain> failed" for everything else. Every thrown error is console.error'd unconditionally so engineers still have the stack.

3. Disable Continue when the SSO is already linked to this identity

SignInWithSso takes a new optional openIdCredentials prop. In the add-access-method flow, AddAccessMethodWizard passes the identity's current credentials through; once two-hop discovery reveals the SSO's (iss, aud), a $derived check disables the Continue button with an inline hint if that SSO is already linked. Mirrors how AddAccessMethod.svelte already disables direct-provider buttons for already-linked providers. Left undefined in the sign-in flow, where reusing an existing credential is the point.

This makes reaching OpenIdCredentialAlreadyRegistered for this identity impossible, so the OpenIdCredentialAlreadyLinkedHereError specialization in linkOpenIdAccount and the corresponding toaster branch in handleError are dropped as unreachable.

4 + 5. Move SSO domain + name from FE localStorage to canister-stamped credential metadata

ssoDomainStorage.ts (a per-device localStorage map of (iss, sub, aud) → domain) is removed. The canister now stamps two new metadata keys on any credential verified by a DiscoverableProvider:

  • sso_domain — the discovery_domain the user entered at sign-up. The canonical SSO label; always present for SSO credentials.
  • sso_name — optional human-readable name from {domain}/.well-known/ii-openid-configuration. When the domain publishes name: "DFINITY", the access-methods list reads "DFINITY account" instead of "dfinity.org account".

BE:

  • IIOpenIdConfiguration hop-1 schema gets name: Option<String> (with #[serde(default)] so older deployments still parse).
  • DiscoverableProvider and DiscoveryState carry discovery_domain and a discovered_name ref that hop-1 populates each refresh.
  • DiscoverableProvider::verify() inserts the two keys before returning the credential.

FE:

  • openIdName now resolves sso_namesso_domainfindConfig(iss, aud, metadata).name, so an SSO-via-Google credential reads as "DFINITY" (or "dfinity.org" if the domain doesn't publish a name), not "Google".
  • openIdLogo returns undefined when sso_domain is present (generic SSO icon).
  • OpenIdItem.svelte detects SSO via sso_domain directly, instead of the previous "no logo found" heuristic that was brittle for direct-provider credentials whose issuer didn't match any openid_configs entry.
  • authFlow#continueWithSso and addAccessMethodFlow#linkSsoAccount both simplify — no pre-decoding the JWT or remembering the domain locally — because the canister handles the labeling end-to-end, so the mapping survives reloads and crosses devices.

Tests

  • cargo test -p internet_identity --bin internet_identity: 219/219 pass.
  • cargo clippy --all-targets -D warnings: clean.
  • cargo fmt --check: clean.
  • npm run check (tsc + svelte-check): 0 errors (19 pre-existing warnings unchanged).
  • npm run lint + prettier --check: clean.

< Previous PR

Copilot AI review requested due to automatic review settings April 24, 2026 11:36
@aterga aterga requested a review from a team as a code owner April 24, 2026 11:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Follow-up SSO UX + data-model refinements on top of #3785, including improved labeling (“Continue with SSO”), simplifying user-facing error copy, preventing linking an SSO already attached to the current identity, and moving SSO labeling from FE localStorage to canister-stamped credential metadata.

Changes:

  • Backend: make the SSO allowlist depend on is_production, persist/replay discoverable OIDC configs across upgrades, and stamp sso_domain / optional sso_name into verified credential metadata.
  • Frontend: add SSO discovery + UI flow, render SSO credentials using stamped metadata, and improve OAuth callback error handling + provider resolution via strict (iss,aud) matching.
  • Cleanup: remove deprecated oidc_configs from init/synchronized config surfaces and adjust tests/types/IDL accordingly.

Reviewed changes

Copilot reviewed 38 out of 40 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/internet_identity_interface/src/internet_identity/types.rs Removes deprecated oidc_configs from init + synchronized config types.
src/internet_identity/tests/integration/http.rs Updates synchronized-config assertion after removing oidc_configs.
src/internet_identity/tests/integration/config/oidc_configs.rs Aligns integration tests with prod/beta allowlist split and new surfacing via discovered_oidc_configs.
src/internet_identity/src/openid/generic.rs Makes allowlist environment-dependent; carries/stamps discovery_domain + optional name into credential metadata.
src/internet_identity/src/main.rs Stops returning oidc_configs in config(); rehydrates in-memory SSO providers from persistent state on init/upgrade.
src/internet_identity/src/assets.rs Ships only openid_configs in /.config.did.bin (frontend no longer needs oidc_configs).
src/internet_identity/internet_identity.did Removes deprecated oidc_configs from init args in DID.
src/frontend/src/routes/(new-styling)/manage/(authenticated)/(access-and-recovery)/access/components/OpenIdItem.svelte Renders SSO credentials via sso_domain marker and shows SSO icon; uses displayName.
src/frontend/src/routes/(new-styling)/manage/(authenticated)/(access-and-recovery)/access/+page.svelte Passes sub/aud into openIdName for better provider resolution.
src/frontend/src/routes/(new-styling)/authorize/+page.svelte Uses aud when resolving provider config.
src/frontend/src/lib/utils/ssoDiscovery.ts Adds two-hop SSO discovery implementation with validation, caching, rate-limits, retries.
src/frontend/src/lib/utils/ssoDiscovery.test.ts Adds unit tests for SSO discovery validation, security checks, caching, and error wrapping.
src/frontend/src/lib/utils/openID.ts Adds OAuthProviderError, extractIdTokenFromCallback, strict (iss,aud) provider resolution, SSO metadata name/logo logic, scope selection helper.
src/frontend/src/lib/utils/openID.test.ts Updates tests for new config resolution + new helpers; adds callback parsing tests.
src/frontend/src/lib/locales/en.po Adds new SSO-related strings and updates account copy to use {displayName}.
src/frontend/src/lib/locales/de.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/es.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/fr.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/id.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/it.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/nl.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/pl.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/ru.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/uk.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/locales/ur.po Adds new SSO-related strings (untranslated placeholders) and updates {displayName} strings.
src/frontend/src/lib/globals.ts Refactors OpenID email verification IDL variant into a reusable constant and formats type.
src/frontend/src/lib/generated/internet_identity_types.d.ts Updates generated types for discoverable OIDC config + discovered configs methods/types.
src/frontend/src/lib/generated/internet_identity_idl.js Updates generated IDL for discoverable OIDC config + discovered configs query.
src/frontend/src/lib/flows/authLastUsedFlow.svelte.ts Calls findConfig with aud as undefined (issuer-only fallback).
src/frontend/src/lib/flows/authFlow.svelte.ts Adds signInWithSso view + continueWithSso using synthetic OpenID config and selected scopes.
src/frontend/src/lib/flows/addAccessMethodFlow.svelte.ts Adds SSO view + linkSsoAccount for access-method linking via discovery result.
src/frontend/src/lib/components/wizards/auth/views/SignInWithSso.svelte Adds SSO domain entry screen with debounced registration+discovery and actionable error mapping.
src/frontend/src/lib/components/wizards/auth/views/PickAuthenticationMethod.svelte Adds SSO entry button and changes button layout; updates aria-label.
src/frontend/src/lib/components/wizards/auth/AuthWizard.svelte Wires SSO view into auth wizard and hooks continueWithSso.
src/frontend/src/lib/components/wizards/addAccessMethod/views/AddAccessMethod.svelte Adds SSO option button in add-access-method wizard.
src/frontend/src/lib/components/wizards/addAccessMethod/AddAccessMethodWizard.svelte Wires SSO view into add-access-method wizard and passes existing credentials for “already linked” check.
src/frontend/src/lib/components/utils/error.ts Adds toast handling for OAuthProviderError surfaced from callback fragments.
src/frontend/src/lib/components/ui/ManageIdentities.svelte Passes sub and aud (undefined) into openIdName for last-used identities.
src/frontend/src/lib/components/ui/IdentityAvatar.svelte Passes sub and aud (undefined) into openIdLogo for last-used identities.
src/frontend/src/lib/components/icons/SsoIcon.svelte Adds an SSO icon component.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/frontend/src/lib/components/utils/error.ts Outdated
Comment thread src/frontend/src/lib/utils/openID.test.ts Outdated
Comment thread src/frontend/src/lib/utils/ssoDiscovery.ts Outdated
Comment thread src/frontend/src/lib/utils/ssoDiscovery.ts Outdated
aterga and others added 3 commits April 24, 2026 13:42
The SSO entry in PickAuthenticationMethod and AddAccessMethod leads
into a flow that can end in either a sign-in or a sign-up, so "Sign
in with SSO" is misleading. Rename the `aria-label` to "Continue with
SSO" — matches the copy pattern used by the direct-provider buttons
and the Continue button inside the SSO screen itself. The SignInWithSso
screen's h1 is updated separately in the following commit.

Addresses sea-snake's review comment on dfinity#3785.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four related fixes from sea-snake's review, best reviewed together
since they overlap on the same files:

1. **Slim SSO error mapping to user-actionable copy.** `mapSubmitError`
   in `SignInWithSso.svelte` had a ladder of branches for every
   provider-misconfiguration shape (hostname mismatch, non-HTTPS
   issuer / auth endpoint, malformed discovery doc, ...). These
   read as implementation-detail leakage to end users. Keep only the
   cases the user (or their SSO admin) can act on — domain-not-
   configured, canary allowlist, OAuth provider errors from the
   callback fragment, rate limits — and fall back to a generic
   "SSO sign-in for <domain> failed" for everything else. Always
   `console.error` the raw error so engineers still have the stack.

2. **Disable Continue when the SSO is already linked to this
   identity.** `SignInWithSso.svelte` takes an optional
   `openIdCredentials` prop (passed in the add-access-method context,
   left `undefined` in the sign-in context). After two-hop discovery
   reveals the SSO's `(iss, aud)`, a `$derived` `isAlreadyLinked`
   checks against the prop and disables the Continue button with
   an inline hint. Mirrors the direct-provider (Google/Apple/...)
   button-disable in `AddAccessMethod.svelte` — reaching the
   canister's `OpenIdCredentialAlreadyRegistered` for this identity
   is no longer possible, so the `OpenIdCredentialAlreadyLinkedHereError`
   specialization in `linkOpenIdAccount` / `handleError` is removed
   as unreachable.

3. **Move SSO domain + name from FE localStorage to canister-stamped
   metadata.** The previous iteration used `ssoDomainStorage.ts` (a
   per-device localStorage map of `(iss, sub, aud) → domain`) to
   label SSO credentials by the domain the user typed. That's
   unreliable across devices and a lot of FE bookkeeping for something
   the canister already has. The canister now stamps two new metadata
   keys on any credential verified by a `DiscoverableProvider`:

   - `sso_domain` — the `discovery_domain` the user entered
     (canonical SSO label; always present for SSO credentials).
   - `sso_name` — the `name` field from
     `{domain}/.well-known/ii-openid-configuration` if the domain
     publishes one (e.g. `"DFINITY"`). Optional.

   The `IIOpenIdConfiguration` hop-1 schema gets a new optional
   `name: Option<String>` (serde `#[serde(default)]` so older
   deployments that don't publish the field still parse).
   `DiscoverableProvider` / `DiscoveryState` carry `discovery_domain`
   and a `discovered_name` ref that hop-1 populates each refresh.

4. **Render the SSO `name` (with domain fallback).** `openIdName`
   now reads `sso_name` → `sso_domain` → `findConfig(iss, aud).name`,
   so an SSO credential linked via a domain that publishes
   `name: "DFINITY"` renders as "DFINITY", and one that doesn't
   renders as "dfinity.org". `openIdLogo` returns `undefined` when
   `sso_domain` is present (generic SSO icon). `OpenIdItem.svelte`
   detects SSO via `sso_domain` directly instead of the "no logo
   found" heuristic — which was brittle for direct-provider credentials
   whose issuer didn't match any `openid_configs` entry.

Removes `ssoDomainStorage.ts` and its callers in
`authFlow.svelte.ts` and `addAccessMethodFlow.svelte.ts`. The SSO
sign-in flow no longer needs to pre-decode the JWT either — the
canister handles labeling — so `continueWithSso` collapses to
`return this.continueWithOpenId(syntheticConfig)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locales re-extracted via `npm run extract` after the SSO copy change
("Sign in with SSO" → "Continue with SSO") and the slimmed
`mapSubmitError` output dropped several now-unused strings. New
source strings in en.po + empty msgstr placeholders in the 10
non-English catalogs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aterga aterga force-pushed the sso-review-fixes-3785 branch from b5d8994 to 5145d7a Compare April 24, 2026 11:47
Five small fixes from Copilot's pass on the follow-up PR:

1. `error.ts`: the `OAuthProviderError` toast branch is hit by both
   SSO and direct-provider OAuth failures, so "SSO provider returned"
   was misleading for direct Google/Apple/Microsoft. Broadened to
   "OAuth provider returned" + generic admin-hint in the description.

2. `openID.test.ts`: dropped the stale comment that still referred
   to the removed localStorage SSO map — point at the canister-
   stamped `sso_domain` / `sso_name` metadata instead.

3. `ssoDiscovery.ts` header: updated the reference from the now-
   removed `ALLOWED_DISCOVERY_DOMAINS` const to the
   `allowed_discovery_domains()` function.

4. `OpenIdItem.svelte`: guard against `logo === undefined` in the
   non-SSO branch so the generic `SsoIcon` renders as a fallback
   instead of `{@html logo}` stringifying `undefined` into the DOM.
   This covers direct-provider credentials whose issuer doesn't
   match any `openid_configs` entry (e.g. after client_id rotation).

5. `ssoDiscovery.ts` `fetchWithRetry`: classify errors so only
   transient failures (network errors, HTTP 408 / 429 / 5xx) are
   retried with backoff. Deterministic 4xx (404, 400, 401, 403, ...)
   fail fast — retrying a `404` on a domain that doesn't publish
   `/.well-known/ii-openid-configuration` three times just wastes
   2s + 4s + 8s before the UI can surface the same error. Added a
   retry-count assertion to the 404 test (now 1 fetch, not 3) and a
   new test for the 503 retry path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aterga aterga requested a review from sea-snake April 24, 2026 12:14
Comment thread src/internet_identity/src/openid/generic.rs Outdated
Comment thread src/internet_identity/src/openid/generic.rs Outdated
…expose as optional fields

sea-snake's two remaining threads on dfinity#3803:

1. **"This shouldn't be metadata; look up SSO domain + name by (iss, aud)
   and return them as optional fields on the credential when requested."**
   Done. `OpenIdCredentialData` (the Candid type returned by
   `get_anchor_info` and friends) gains two optional fields,
   `sso_domain` and `sso_name`. They are NOT stored — `DiscoverableProvider::
   verify` no longer stamps them into the credential metadata. Instead, a
   new helper `openid::generic::sso_fields_for(iss, aud)` consults the
   current `DISCOVERY_TASKS` state; the `OpenIdCredential →
   OpenIdCredentialData` conversion in `storage/anchor.rs` calls it for
   every credential on the way out.

   Benefits over the previous metadata-stamping approach:
   - If an SSO admin updates the `name` in `/.well-known/ii-openid-
     configuration`, existing credentials pick it up on the next
     `get_anchor_info` — no stored-metadata migration needed.
   - If an SSO is de-registered from the canister allowlist, old
     credentials still exist but their `sso_domain` / `sso_name` come
     back `None`, which is the truthful "we don't know anything about
     this SSO anymore" signal.
   - Metadata stays a place for per-credential facts from the JWT
     (email, name, email_verified, tid, ...), not for cross-credential
     policy state.

2. **"Fallback should be done where needed in the frontend, not backend.
   This lets the frontend tell 'has a name' apart from 'only a domain'
   for future divergent rendering."**
   Done. Both the canister and the FE now carry `sso_name` and
   `sso_domain` as independent `Option<>` / `[] | [string]` values —
   the BE never collapses one into the other. `openIdName` resolves
   `ssoName → ssoDomain → findConfig(iss, aud).name` on the FE.
   `openIdLogo` returns `undefined` iff `ssoDomain` is present.

Incidental cleanup:
- `DiscoverableProvider` drops its `discovery_domain` and
  `discovered_name` fields; those only need to live on the matching
  `DiscoveryState` (which is where the discovery timer writes them and
  where `sso_fields_for` reads them from).
- Regenerated `internet_identity.did`, `internet_identity_types.d.ts`,
  and `internet_identity_idl.js` for the new optional fields.
- `openIdName` / `openIdLogo` signatures drop the unused `sub`
  parameter and take `ssoName` / `ssoDomain` explicitly. `LastUsedIdentity`-
  backed callers (`IdentityAvatar`, `ManageIdentities`) pass `undefined`
  — correct for direct providers, imprecise for SSO until dfinity#3795 tracks
  `aud` + SSO metadata on `LastUsedIdentity`.

Tests: `cargo test -p internet_identity --bin internet_identity` → 219/219 pass;
`cargo clippy --all-targets -D warnings`, `cargo fmt --check`, `npm run
check`, `npm run lint`, `npm run format` all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aterga aterga enabled auto-merge April 24, 2026 13:10
Comment thread src/internet_identity/src/storage/anchor.rs Outdated
sea-snake
sea-snake previously approved these changes Apr 24, 2026
@aterga aterga added this pull request to the merge queue Apr 24, 2026
…ration

sea-snake's latest review on dfinity#3803:
> These two fields should be together in a single struct assigned to a
> field called `sso_configuration`.

Introduce `SsoConfiguration { domain: String, name: Option<String> }`
and replace the two independent optional fields on
`OpenIdCredentialData` with a single `sso_configuration :
opt SsoConfiguration`. Semantics:

- `sso_configuration: None` → not an SSO credential (direct Google /
  Apple / Microsoft, or an SSO that's no longer registered on the
  canister).
- `sso_configuration: Some({ domain, name })` → SSO credential; `domain`
  is always known, `name` only if the domain publishes it. Keeping the
  two cases distinct inside the inner struct preserves the "has a name
  vs. only a domain" signal the FE may want to render differently.

`openid::generic::sso_fields_for(iss, aud) -> (Option<String>,
Option<String>)` is replaced by
`openid::generic::sso_configuration_for(iss, aud) ->
Option<SsoConfiguration>`; the storage-layer conversion sets the field
in one line. `.did` / TS bindings / FE callers (`OpenIdItem`,
`access/+page`, `addAccessMethodFlow`, `utils.test.ts`) follow.

`openIdName` / `openIdLogo` keep their `(ssoName?, ssoDomain?)` param
shape — callers just unwrap `sso_configuration` and pass its parts in.
That keeps the fallback (`ssoName → ssoDomain → findConfig`) on the FE
side, per the earlier review thread.

Tests: cargo test 219/219 pass, clippy + fmt clean, svelte-check 0
errors, lint + prettier clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot dismissed sea-snake’s stale review April 24, 2026 13:29

Review dismissed by automation script.

Merged via the queue into dfinity:main with commit f07204c Apr 24, 2026
5 checks passed
@aterga aterga deleted the sso-review-fixes-3785 branch April 24, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants