@Mail Domain Check

ZITADEL 4.15.3 | Login V2 | Registration gate

ZITADEL Login V2 Registration Skips Email Verification

If password reset and manual verification emails work but Login V2 self-registration authenticates immediately, SES is not the first boundary. Prove the enforcement flag inside the separate Login container, then keep persisted email state, automatic message generation, and application-session completion as independent checks.

Fastest configuration check

ZITADEL Login 4.15.3 defaults EMAIL_VERIFICATION=false. The registration helper redirects an unverified user only when the running Login process sees the value true.

Treat four boundaries separately

Enforcement configuration
EMAIL_VERIFICATION=true must reach the running ghcr.io/zitadel/zitadel-login process. Setting it on the core ZITADEL service does not configure the separate Login application.
Persisted user state
Login 4.15.3 calls AddHumanUser with isVerified=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.

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

  1. Record the running Login container image tag or digest. Do not infer it from the core ZITADEL version.
  2. Check only whether EMAIL_VERIFICATION exists and equals true in the running Login container. Do not dump the full environment.
  3. Set the flag on the Login service, then recreate or restart that container so deployment input and runtime state agree.
  4. 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.
  5. Create one new controlled account, read back its email verification boolean before code use, and confirm no application session or usable token completes.
  6. 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

  1. With EMAIL_VERIFICATION=true in the running Login container, create one controlled user and assert the first stored state is unverified.
  2. Assert registration cannot complete an application session or issue a usable application token before verification.
  3. Assert one registration produces one verification message and one current code.
  4. Assert the valid code changes verification state exactly once and only then permits login.
  5. Assert replay, stale, malformed, and wrong-user codes remain rejected.
  6. Assert password reset and manual resend still work without masking an automatic-registration-send regression.
Another verification failure?

Build a browser-local plan across application state, message handoff, provider delivery, receiver evidence, and one-time completion without entering an address or token.

Build verification plan

Primary references