<!-- Please inform yourself about the contribution guidelines on submitting a PR here: https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr. Take note of how PR/commit titles should be written and replace the template texts in the sections below. Don't remove any of the sections. It is important that the commit history clearly shows what is changed and why. Important: By submitting a contribution you agree to the terms from our Licensing Policy as described here: https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions. --> # Which Problems Are Solved An OIDC authorize request that carries a `login_hint` (and no `prompt`) is meant to land the user on a **prefilled** login screen and let them proceed straight to authentication. Two problems prevented that: **1. The hint was dropped when an unrelated session existed.** When the browser held a `sessions` cookie for a *different* account: - The user was redirected to the **account picker** (`/accounts`) instead of the prefilled login-name screen. - The picker listed the already-logged-in, unrelated account(s) — never the user identified by `login_hint`. - The `login_hint` was **silently dropped** (the `/accounts` URL carries no hint), so the typed email was lost and the flow dead-ended on the wrong accounts. In a clean session (no `sessions` cookie) the same request prefilled correctly, which made the bug depend on unrelated pre-existing sessions. **2. The prefilled path was jarring and looped on "back".** Even when the hint *was* honoured, the flow redirected to `/loginname?loginName=…&submit=true`. That page renders the username screen and then **client-side auto-submits** it, so the user sees the username flash on screen, watches "Continue" click itself, and only then lands on the password step. Worse, `/loginname?submit=true` stays in browser history, so pressing **Back** from the password screen re-triggers the auto-submit and bounces the user straight forward again — an inescapable loop with no way to edit the username. # How the Problems Are Solved **Honour the hint (problem 1).** In `handleOIDCFlowInitiation`, the default branch (no `prompt`) already calls `findValidSession`, which filters the browser's sessions by `login_hint` and treats the hint as *selecting the user*. When the hint matches none of the sessions it returns `undefined`, and the remaining `eligibleSessions` (filtered by organization only) are therefore never the hinted user. The fallback previously chose between the loginname screen and the account picker based solely on `eligibleSessions.length === 0`, so a present-but-unrelated session sent the user to `gotoAccounts`, discarding the email. The loginname branch is now also gated on `authRequest.loginHint`, mirroring the existing empty-eligible-sessions branch. **Skip the flash and the loop (problem 2).** Instead of bouncing through `/loginname?submit=true` and relying on client-side auto-submit, the hint is now resolved **server-side** via `sendLoginname` (the same approach the `Prompt.LOGIN` branch already used) and the user is redirected **straight to the next step** (e.g. `/password`), skipping the `/loginname` screen entirely. This is factored into a shared `resolveLoginHint` helper and applied to all `login_hint` branches (default-prompt, no-session, and the existing `Prompt.LOGIN` path, now deduplicated). When the hint can't be resolved (unknown/ambiguous user, transient error), the flow falls back to a prefilled `/loginname` **without** `submit=true` — a clean screen where the user clicks once — so there is no auto-submit and no back-button loop in any path. ```ts if (!selectedSession || !selectedSession.id) { // login_hint matches no session: resolve it straight to the next step. if (authRequest.loginHint) { const hintResponse = await resolveLoginHint({ request, requestId, loginHint: authRequest.loginHint, organization }); if (hintResponse) { return hintResponse; } } // Prefill loginname (not the account picker) for an unresolved hint or when no session is eligible. if (authRequest.loginHint || eligibleSessions.length === 0) { return gotoLoginname({ request, requestId, loginHint: authRequest.loginHint, organization, orgDomain }); } return gotoAccounts({ request, requestId, organization, orgDomain }); } ``` Resulting behaviour: - `login_hint` set, a session **matches** it → silent auth (**unchanged**). - `login_hint` set, **no** session matches, hint **resolves** → straight to the next step (e.g. `/password`), no `/loginname` flash (**was**: account picker with the hint dropped / a jarring auto-submit screen). - `login_hint` set, hint **can't** be resolved → prefilled `/loginname`, no auto-submit, Back stays editable. - **No** `login_hint` → account picker / loginname exactly as before (**unchanged**). # Additional Changes - `resolveLoginHint` helper extracted in `flow-initiation.ts`; the `Prompt.LOGIN` branch now reuses it instead of duplicating the `sendLoginname` call. - `submit=true` is no longer emitted for the login-name flow, so the client-side auto-submit in `username-form.tsx` is no longer reached from OIDC initiation. - Tests in `flow-initiation.test.ts`: each `login_hint` branch (org-scope-filtered, unrelated-session, no-session) is covered twice — hint resolves → straight to `/password`; hint unresolved → prefilled `/loginname` with **no** `submit=true`. # Additional Context - The org-only `eligibleSessions` check was introduced in #12346, which correctly handled the *empty* case but did not account for a present-but-unmatched `login_hint`; this PR extends that gate. - Per OIDC, `login_hint` is advisory prefill; a client that wants the chooser would send `prompt=select_account`. Resolving the hint server-side also matches how the `Prompt.LOGIN` branch already behaved. --------- Co-authored-by: Max Peintner <[email protected]> Co-authored-by: Max Peintner <[email protected]>
The Identity Infrastructure for Developers
ZITADEL is an open-source identity and access management platform built for teams that need more than basic auth. Whether you're securing a SaaS product, building a B2B platform, or self-hosting a production IAM stack — ZITADEL gives you everything out of the box: SSO, MFA, Passkeys, OIDC, SAML, SCIM, and a battle-tested multi-tenancy model.
No vendor lock-in. No compromise on control. Just a robust, API-first identity platform you can own.
🏡 Website | 💬 Chat | 📋 Docs | 🧑💻 Blog | 📞 Contact
Why ZITADEL
We built ZITADEL to handle the hardest IAM challenges at scale — starting with multi-tenancy.
| ZITADEL | FusionAuth | Keycloak | Auth0/Okta | |
|---|---|---|---|---|
| Open-source | ✅ | ❌ | ✅ | ❌ |
| Self-hostable | ✅ | ✅ | ✅ | ❌ |
| Infrastructure-level tenants | ✅ Instances (High scale) | ✅ Tenants | 🟡 Realms (Scaling limits) | ❌ (Multi-tenant = multi-account) |
| B2B Organizations | ✅ Native & Unlimited | 🟡 via Entity Management | ✅ (Recent addition) | 🟡 (Plan/Account dependent) |
| Full audit trail | ✅ Comprehensive Event Stream* | 🟡 Audit logs | 🟡 Audit logs | 🟡 Audit logs |
| Passkeys (FIDO2) | ✅ | ✅ | ✅ | ✅ |
| Actions / webhooks | ✅ | ✅ | 🟡 via SPI | ✅ |
| API-first (gRPC + REST) | ✅ | 🟡 REST only | 🟡 REST only | 🟡 REST only |
| SaaS + self-host parity | ✅ | ✅ | ➖ N/A | ➖ N/A |
ZITADEL Cloud and self-hosted ZITADEL run the same codebase.
Key differentiators for architects:
- Relational core, event-driven soul — every mutation is written as an immutable event for a complete, API-accessible audit trail. Unlike systems that log only select activities, ZITADEL provides a comprehensive event stream that can be audited or streamed to external systems via Webhooks.
- Strict multi-tenant hierarchy — Identity System → Organizations → Projects, with isolated data and policy scoping at multiple levels
- API-first design — every resource and action is available via connectRPC, gRPC, and HTTP/JSON APIs
- Zero-downtime updates and horizontal scalability without external session stores
Get Started in 3 Minutes
ZITADEL Self-Hosted
# Docker Compose — up and running in under 3 minutes
curl -LO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.yml \
&& curl -LO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/.env.example \
&& cp .env.example .env \
&& docker compose up -d --wait
Full deployment guides:
Need professional support for your self-hosted deployment? Contact us.
ZITADEL Cloud (SaaS)
Start for free at zitadel.com — no credit card required. Available in US · EU · AU · CH. Pay-as-you-go pricing.
Integrate with the V2 API
ZITADEL exposes every capability over a typed API. Here's how to create a user with the V2 REST API:
curl -X POST https://$ZITADEL_DOMAIN/v2/users/human \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "[email protected]",
"profile": { "givenName": "Alice", "familyName": "Smith" },
"email": { "email": "[email protected]", "sendCode": {} }
}'
Explore the full API reference — including connectRPC and gRPC transports — or jump straight to quickstart examples.
Features
Authentication
- Single Sign On (SSO) · Username/Password · Passkeys (FIDO2 / WebAuthn)
- MFA: OTP, U2F, OTP Email, OTP SMS
- LDAP · Enterprise IdPs and social logins
- OpenID Connect certified · SAML 2.0 · Device authorization
- Machine-to-machine: JWT Profile, PAT, Client Credentials
- Token exchange and impersonation
- Custom sessions for flows beyond OIDC/SAML
- Hosted Login V2
Multi-Tenancy
- Identity brokering with pre-built IdP templates
- Customizable B2B onboarding with self-service for customers
- Delegated role management to third parties
- Domain discovery
Integration
- gRPC, connectRPC, and REST APIs for every resource
- Actions: webhooks, custom code, token enrichment
- RBAC · SCIM 2.0 Server
- Audit log and SOC/SIEM integration
- SDKs and example apps
Self-Service & Admin
- Self-registration with email/phone verification
- Administration Console for orgs and projects
- Custom branding per organization
Deployment
- PostgreSQL (≥ 14) · Zero-downtime updates · High scalability
Track upcoming features on our roadmap and follow our changelog for recent updates.
Showcase
Login V2
Our new, fully customizable login experience — documentation
Adopters & Ecosystem
Used in production by organizations worldwide. See the full Adopters list — and add yours by submitting a pull request.
- SDKs: All supported languages and frameworks
- Examples: Clone and use our examples
How To Contribute
ZITADEL is built in the open and welcoming to contributions of all kinds.
- 📖 Read the Contribution Guide to get started
- 💬 Join the conversation on Discord
- 🐛 Report bugs or request features via GitHub Issues
Contributors
Made with contrib.rocks.
Security
Security policy: SECURITY.md
Vulnerability Disclosure Policy — how to responsibly report security issues.
Technical Advisories are published for major issues that could impact security or stability in production.
License
AGPL-3.0 — see LICENSING.md for the full licensing policy, including Apache 2.0 and MIT exceptions for specific directories.


