* feat(login): add shared enrollment authorization guard Introduce assertSessionAuthorizedForEnrollment, a single gate that decides whether a session may enroll a new authenticator: it requires a verified primary factor (password, passkey or IDP) on a non-expired session, or, for onboarding, a user with no auth methods plus a prior user-verification check. This mirrors the gate previously inlined only in registerPasskeyLink, which is refactored to reuse the shared helper. It is the foundation for gating the remaining credential-enrollment entry points (GHSA-45f2-5q3r-xgg6). Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): require authentication before WebAuthn/U2F enrollment addU2F and verifyU2F registered a credential for any session that resolved to a user id, including a bare identify-only session created by merely submitting a login name. An unauthenticated attacker who knew a victim's login name could therefore attach an attacker-controlled authenticator to the victim's account and then complete a login as the victim. Both actions now run assertSessionAuthorizedForEnrollment before registering or verifying, so an unauthenticated session is rejected and no credential is persisted. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): require authentication before TOTP/OTP enrollment verifyTOTP and the otp/<method>/set page activated a TOTP/OTP factor for any session with a user id, and the u2f/set page rendered the registration form without an authorization check. An identify-only session could thus enroll a second factor on a victim's account. Gate all of them with assertSessionAuthorizedForEnrollment. The setup pages now render the unknown-context/error state instead of enrolling when the session is not authorized. Also guard the method value in the OTP set page, since the added guard branch removes the control-flow narrowing the previous throw provided. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): require authentication before legacy (V1) passwordless enrollment The V1 passwordless registration handlers began and verified passwordless setup for any auth request that carried a user id, without checking that a first factor had been verified. An unauthenticated attacker could submit a victim's login name and then POST directly to the passwordless prompt/init endpoints to plant an attacker-controlled passkey, then log in as the victim. Add passwordlessSetupAllowed / CheckPasswordlessSetupAllowed, which permit enrollment only for a legitimate onboarding user (PasswordlessInitRequired) or an auth request whose first factor is already verified, and call it before BeginPasswordlessSetup and VerifyPasswordlessSetup. The verify path is also bound to authReq.UserID instead of the attacker-controllable form field, so a credential cannot be redirected onto a different account. The init-code (mail/onboarding) path is unchanged. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): remove never-active legacy (V1) authReq passwordless setup path Supersedes the runtime gate added in the previous commit. Maintainer review (Livio, the original author) and investigation confirmed the auth-request based passwordless *setup* path in the legacy login was a never-active leftover: - The passwordless prompt page (passwordless_prompt.html) has no submit button and no script; it only tells the user to complete setup via the emailed link (the PasswordlessButtonText/NextButtonText/SkipButtonText i18n keys are unused). - Legitimate onboarding for a PasswordlessInitRequired user goes through the emailed init-code flow (authReq == nil, BeginPasswordlessInitCodeSetup / VerifyPasswordlessInitCodeSetup), which is untouched. - The authReq branch (handlePasswordlessPrompt -> BeginPasswordlessSetup / VerifyPasswordlessSetup) was reachable only by a crafted direct POST — the GHSA-45f2-5q3r-xgg6 attack. Rather than gate the dead path, remove it entirely, eliminating the attack surface: - delete handlePasswordlessPrompt and the POST /login/passwordless/prompt route (keep renderPasswordlessPrompt: the informational "use your emailed link" page for PasswordlessRegistrationPromptStep), - make renderPasswordlessRegistration / checkPasswordlessRegistration use only the init-code path, - drop the now-unused AuthRequestRepo.BeginPasswordlessSetup / VerifyPasswordlessSetup (and the interface decls) and the passwordlessPromptUrl template helper; the underlying HumanAddPasswordlessSetup / HumanHumanPasswordlessSetup commands remain (used by the auth-API self-service and the v2 passkey APIs). A crafted POST to /login/passwordless/init carrying an authRequestID but no valid code now falls through to the init-code path and fails. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * check for userverified true as well, cleanup return message --------- Co-authored-by: Claude Opus 4.8 <[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.


