[pull] main from dfinity:main#551
Merged
pull[bot] merged 2 commits intomikeyhodl:mainfrom Apr 24, 2026
Merged
Conversation
# Problem Internet Identity users can sign in with personal Google/Apple/Microsoft accounts, but there's no way for an organization to let their employees sign in with *their own* SSO domain (e.g. `alice@dfinity.org`) while still having II act as the identity provider. Each new organization would otherwise require a code change on II to add their issuer, client id, and OAuth endpoints. # Solution A user types their organization's domain on the SSO screen; the frontend calls `add_discoverable_oidc_config` to register the domain (gated by the backend's canary allowlist), then runs a two-hop discovery chain to resolve the provider's OAuth endpoint, then redirects them to sign in. Registration is **user-initiated**: the SSO screen itself drives the canister update call. An II admin still has to land a new domain in the backend's canary allowlist (`openid::generic::allowed_discovery_domains()`) via canister upgrade — what's new is that, once that's done, individual users can register their own org's domain via the SSO screen instead of the config living inline in II's init args. The allowlist is gated on the deployment's `is_production` init flag so the two mainnet canisters don't share a domain: on `id.ai` only `dfinity.org` is accepted, on `beta.id.ai` (and everywhere else — staging, local, CI) only `beta.dfinity.org`. Keeping them disjoint means a DNS takeover of the beta test domain can't backdoor production, and we can stage new IdP wiring on `beta.dfinity.org` without risking the prod issuer. Replaces #3786, which is closed. # Changes ## Backend ### Removed: `oidc_configs` from init args and synchronized config `InternetIdentityInit` previously carried `oidc_configs`, but applying it had never been wired through `apply_install_arg` — it was silently dropped on install/upgrade. Removed the field entirely from `InternetIdentityInit`, `InternetIdentitySynchronizedConfig`, the Candid interface, `config()`, and the `From<&InternetIdentityInit> for InternetIdentitySynchronizedConfig` impl. Registration goes exclusively through the `add_discoverable_oidc_config` update call from here on, and `/.config.did.bin` only carries `openid_configs` (the direct Google/Apple/Microsoft configs). ### `add_discoverable_oidc_config` Traps for any domain not in `ALLOWED_DISCOVERY_DOMAINS`, otherwise inserts the domain into persistent state and kicks off a backend-side two-hop discovery (to populate JWKS for signature verification on subsequent sign-ins). The canister also exposes `discovered_oidc_configs` for querying resolved SSO provider state. ## Frontend **Type alignment with backend.** `DiscoverableOidcConfig` is `{ discovery_domain: string }`. **Two-hop discovery (`ssoDiscovery.ts`, new).** 1. `GET https://{domain}/.well-known/ii-openid-configuration` returns `{ client_id, openid_configuration }`. The domain owner publishes this at their DNS-backed origin. 2. `GET {openid_configuration}` is the provider's standard OIDC discovery, yielding `authorization_endpoint` and `scopes_supported`. Both hops run from the browser. (The backend has its own copy in `openid/generic.rs`; keeping the implementations separate for now minimizes BE↔FE synchronization.) **SSO flow UI.** - `SignInWithSso.svelte` (new): domain input screen. Framed icon chip, title `"Sign In With SSO"`, subtitle `"Enter your company domain"`, placeholder `"company.domain.com"`. Input triggers a debounced (200ms) lookup: `anonymousActor.add_discoverable_oidc_config` (idempotent; traps if the domain isn't on the backend allowlist) → `discoverSsoConfig`. The Continue button enables only once the lookup succeeds; clicking it opens the OAuth popup synchronously from the user gesture (critical for Safari). Canary-allowlist traps are mapped to `"SSO is not available for \"<domain>\" yet."` inline. - `SsoIcon.svelte` (new): key icon. - `PickAuthenticationMethod.svelte`: "Continue with passkey" is the top button; OIDC provider icons + SSO key icon render in the row below at equal width. **The SSO icon is always rendered** — users always see SSO as an option, regardless of whether any domain is registered. - `authFlow.svelte.ts`: new `signInWithSso` view state and `continueWithSso()` method. - Subtitle text across all three sign-in entry points updated from `"Choose method to continue"` → `"Choose an authentication method to continue"`. **SSO vs direct-provider credential disambiguation.** An SSO credential can arrive via the same underlying IdP (e.g. Google) as a direct-Google credential. They differ in `aud` (the `client_id`), but two things broke simple issuer-only matching: 1. Labeling an SSO-via-Google credential as "Google account" (it's not — it's the user's org account). 2. Mis-labeling a legacy direct-Google credential as SSO after `client_id` rotation. Resolved via three-tier lookup: strict `(iss, aud)` match → localStorage SSO-domain map (`{iss, sub, aud} → domain`) → issuer-only fallback. Credentials linked via SSO remember the typed domain in localStorage so they can be surfaced by domain in the access-methods UI on the same device. Cross-device labeling needs backend support and is tracked in #3795. **Safari popup.** The OAuth popup must open synchronously from the click event or Safari blocks it. The SSO lookup (`add_discoverable_oidc_config` + two-hop) is done in a debounced input handler, stashed in local state, and consumed by the click handler with no intervening awaits before `window.open`. **Already-linked disambiguation.** If the backend returns `OpenIdCredentialAlreadyRegistered`, the FE queries `get_anchor_info` and distinguishes "already linked to **this** identity" (specialized `OpenIdCredentialAlreadyLinkedHereError`) from "already linked to another identity" (the generic error). **Cleanup.** `oidc_configs` removed from the frontend's `BackendCanisterConfig` decode schema (no remaining FE consumer after the refactor). Candid is forward-compatible, so the backend continues to accept old init-arg shapes that may have included `oidc_configs`. **Security.** - Domain input DNS-format validated (length, label length, forbidden characters). - Backend canary allowlist (`allowed_discovery_domains()`, gated on `is_production`) is the sole source of truth for which domains can register. The frontend does **not** carry its own copy — the gate lives on the canister where a compromised device can't bypass it. - All three URLs (`/.well-known/ii-openid-configuration`, provider discovery, authorization endpoint) must be HTTPS. - Issuer and `authorization_endpoint` hostnames must match the `openid_configuration` hostname *exactly* or as a true subdomain (prevents a tampered provider-discovery doc from bouncing auth off-host after we've committed to a provider). `endsWith` alone would accept look-alikes like `evildfinity.okta.com`. - Second-hop trust is inherited from the first-hop canary allowlist: an attacker who can tamper with an II-approved domain's `.well-known` has already breached something more fundamental than any FE-side allowlist would catch, and the org knows its own IdP better than II does. - Per-domain rate limit (1 attempt per 10 min), max 2 concurrent discoveries, 4-hour cache per hop, exponential backoff, timeouts with `clearTimeout` in `finally`. # Tests - `config/oidc_configs.rs` covers both branches of the `is_production`-gated allowlist: default install rejects `dfinity.org` (and accepts `beta.dfinity.org`), `is_production: Some(true)` install rejects `beta.dfinity.org` (and accepts `dfinity.org`). `should_coexist_with_openid_configs` now queries SSO state via `discovered_oidc_configs` instead of the removed `config().oidc_configs`. - 23 unit tests in `ssoDiscovery.test.ts`. - 22 unit tests in `openID.test.ts`, including 4 for `selectAuthScopes` and the `(iss, aud)` strict-then-fallback resolution for direct-vs-SSO credentials. - `cargo test -p internet_identity --bin internet_identity`: **219/219 pass**. - `cargo clippy --all-targets -D warnings`: clean. - `npm run lint` + `svelte-check`: clean. --- [< Previous PR](#3784) --------- Co-authored-by: Arshavir Ter-Gabrielyan <arshavir.ter.gabrielyan@dfinity.org> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oposal IDs, fix try-it-out (#3800) ## Summary Close gaps in the release SOP so running `./scripts/make-upgrade-proposal` takes the release engineer from a fresh tag to submitted proposals without pauses to patch around missing sections or ferry values between steps. ## Changes - **Release body now carries `## Try it out`** (staging + testing app URLs) and the full verify-hash command includes `--archive-hash`. Downstream forum post picks up the section for free. Replaces a brittle bash-level insertion in `create_forum_post` that silently failed — the section wasn't actually making it into the post. - **Archive change auto-detected.** New `detect_archive_hash_change` resolves the previous `proposal-backend-*` tag to its matching `release-*` (via `resolve_release_tag`, mirroring the helper in `.github/actions/release/run.sh`), downloads that release's `archive.wasm.gz`, and compares sha256 against the current one. Falls back to "changed" if the previous archive can't be fetched. Replaces the manual "compare hashes and update the release page" prompt. - **Per-proposal verify-hash commands.** Backend proposal emits `--ii-hash X` (plus `--archive-hash Y` iff the archive changed). Frontend proposal emits `--iife-hash Z` only. The release page keeps the canonical full command for anyone verifying the entire release. - **Proposal numbers parsed from `ic-admin` stdout.** Matches `proposal proposal N` (the doubled prefix emitted by `propose_action_from_command` — same pattern as `dfinity/nns-dapp`'s release-sop), with validation that the ID is numeric and ≥ 100000 to guard against false-positive matches on neuron IDs or other numeric output. Removes the two `read -p` prompts. - **`./scripts/verify-hash` accepts any combination of flags.** `--ii-hash` is no longer required; at least one of the three must be provided. All requested targets are built in a single `./scripts/docker-build` invocation, so frontend-only releases and standalone archive verification now work without redundant builds. ## Tests No automated tests added (the repo has no bash test harness). Manually exercised: - `bash -n` on all three scripts. - `parse_proposal_id` on happy path, no-match, too-small ID, empty input, and multiple matches. - `resolve_release_tag` against the existing `proposal-backend-141500` tag (resolves to `release-2026-04-21`) and a nonexistent tag (returns empty). - `detect_archive_hash_change` no-previous-tag fallback in an empty git repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )