Treat four boundaries separately
- Enforcement configuration
EMAIL_VERIFICATION=truemust reach the runningghcr.io/zitadel/zitadel-loginprocess. Setting it on the core ZITADEL service does not configure the separate Login application.- Persisted user state
- Login 4.15.3 calls
AddHumanUserwithisVerified=false. If an immediate readback is already true before code use, investigate a distinct state mutation or observation defect. - Message generation
- Password reset and manual resend prove that a mail transport can send. They do not prove self-registration asked for an initial verification message.
- Session completion
- An unverified account must not receive a usable application session or token when verification is required, even if message delivery is delayed.
Browser-local release gate
Locate the first ZITADEL registration failure
No email address, verification code, action URL, access token, cookie, user ID, instance hostname, SMTP or SES credential, environment dump, or raw log is requested or transmitted.
- First boundary
- Decision
- Next action
- Exit condition
- Safety rule
What ZITADEL 4.15.3 source proves
The official Login application guide makes email verification an opt-in setting: set EMAIL_VERIFICATION=true to require verification before login. The checked-in 4.15.3 Login environment defaults that flag to false.
In addHumanUser, the Login application explicitly creates the email with isVerified=false. Its checkEmailVerification helper redirects only when that user is unverified and the Login process reads the flag as exactly true.
The registration server action creates the user and session before calling that helper. When the flag is absent or false, the helper does not impose a verification redirect. This explains immediate authentication without requiring an SES failure.
If the Console genuinely shows the new email as verified before any code is used, do not fold that observation into the missing gate. It contradicts the Login application's explicit isVerified=false create request and needs its own controlled create-response, immediate readback, and user-event trace.
Apply the bounded correction
- Record the running Login container image tag or digest. Do not infer it from the core ZITADEL version.
- Check only whether
EMAIL_VERIFICATIONexists and equalstruein the running Login container. Do not dump the full environment. - Set the flag on the Login service, then recreate or restart that container so deployment input and runtime state agree.
- Move the Login image to 4.16.1 or newer. The merged initial-send correction performs one awaited server-side send before redirecting, avoiding the older verification-page-triggered duplicate-send path.
- Create one new controlled account, read back its email verification boolean before code use, and confirm no application session or usable token completes.
- Correlate exactly one initial send, one provider outcome, one receiver result, one code use, one false-to-true state change, and the first successful post-verification login.
Why working SES does not close this incident
Password reset, manual verification, and self-registration can invoke different application actions. A successful manual resend proves transport readiness for that action; it does not prove the registration flow enforced verification or requested its initial message.
Keep these outcomes distinct: application requested a send, provider accepted it, receiving server accepted it, inbox displayed it, code completed, and the user gained an application session. Stop at the first boundary without evidence.
Regression contract
- With
EMAIL_VERIFICATION=truein the running Login container, create one controlled user and assert the first stored state is unverified. - Assert registration cannot complete an application session or issue a usable application token before verification.
- Assert one registration produces one verification message and one current code.
- Assert the valid code changes verification state exactly once and only then permits login.
- Assert replay, stale, malformed, and wrong-user codes remain rejected.
- Assert password reset and manual resend still work without masking an automatic-registration-send regression.
Build a browser-local plan across application state, message handoff, provider delivery, receiver evidence, and one-time completion without entering an address or token.
Primary references
- ZITADEL issue 12474: Login V2 self-registration skips email verification
- ZITADEL Login application configuration guide
- ZITADEL Login 4.15.3 default environment
- ZITADEL Login 4.15.3 AddHumanUser request
- ZITADEL Login 4.15.3 email-verification helper
- ZITADEL Login 4.15.3 registration server action
- ZITADEL pull request 11995: move the initial send to one server-side action
- ZITADEL Login 4.16.1 verification helper