Skip to content

[pull] main from dfinity:main#551

Merged
pull[bot] merged 2 commits intomikeyhodl:mainfrom
dfinity:main
Apr 24, 2026
Merged

[pull] main from dfinity:main#551
pull[bot] merged 2 commits intomikeyhodl:mainfrom
dfinity:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Apr 24, 2026

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 : )

timothyaterton and others added 2 commits April 24, 2026 09:46
# 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>
@pull pull Bot locked and limited conversation to collaborators Apr 24, 2026
@pull pull Bot added the ⤵️ pull label Apr 24, 2026
@pull pull Bot merged commit eb19dfe into mikeyhodl:main Apr 24, 2026
1 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants