fix(web): Ever ID "verify your e-mail" instead of AccessDenied + "Login with Ever ID" button

Merged
EV
10d ago
+729-78

Ever ID: "verify your e-mail" instead of AccessDenied, and the "Login with Ever ID" button

Description

The bug. On 2026-10-17 at 20:27Z and 20:29Z an Ever ID sign-in on app.ever.team ended on
/auth/error?error=AccessDenied. The web logged ever_id.signin outcome=rejected status=401 twice; the API
(POST /api/auth/zitadel/token) answered 401.

The refusal reason (checked, not guessed). The Ever ID account had been created at 20:26:24Z and its e-mail address
was not verified (Ever ID user record, read-only lookup). Gauzy's ZitadelSigninService.decide() returns
email_unverified first thing when the ID token's email_verified is not true, and the token route answers that with
the same bare 401 it uses for a bad token. The audience was NOT the cause: the web's client id is the only entry of the
API's ZITADEL_ALLOWED_AUDIENCES (prod and stage), and the client has id_token_userinfo_assertion = true, so the ID
token carries email / email_verified.

(Why an unverified person gets a token at all: Ever ID's hosted login does not enforce e-mail verification. That is an
Ever ID setting, outside this repo; every product has to handle the case anyway.)

What Was Changed

Major Changes

  • 401 + unverified e-mail → /auth/error?error=EverIdEmailUnverified: "Verify your Ever ID e-mail", with
    "Verify my e-mail on Ever ID" and "Sign in with Ever ID again" (and "Sign in another way"). Triggered when the verified
    ID token's email_verified is not true, or when the API answers { code: 'email_unverified' } (a matching Gauzy
    change adds that code; this PR does not need it).
  • GET /api/auth/ever-id/verify-email sends the browser to Ever ID's verification form
    (<issuer>/ui/v2/login/verify?userId=<sub>), where the person enters the code Ever ID e-mailed them or has a new one
    sent. The subject comes from the sealed httpOnly cookie the refused sign-in set (new sealed step verify_email, same
    AES-256-GCM cookie as the confirm / sign-up steps), never from a URL of this app. No cookie → back to the sign-in.
    404 while Ever ID is off.
  • Log line: ever_id.signin outcome=email_unverified, and the remaining rejections carry a fixed
    reason=no_id_token|token_refused|id_token_required. Still no token, subject or e-mail in any line (tests pin it).
  • "Login with Ever ID" button, per the Ever brand kit (ever-co/platform packages/brand, PR #37): a row of its
    own above the other providers, as wide as the form's primary "Continue" button, 40 px, radius 8. The kit's
    rotating star (the ever.works logo star, one turn every 9 s, no spin with prefers-reduced-motion), "Login with",
    and the kit's "ever® ID" wordmark. Light: #0A0A0A with white text (hover #262626); dark: #EDEDED with
    #0A0A0A text (hover #FFF); focus ring 2 px (#231645 / #CC66FF). Written as Tailwind classes so it follows
    this app's theme switch (the kit's global CSS sets :root tokens). Screen readers read "Login with Ever ID".
    Login (passcode / password) and register pages, since they share the component.

Minor Changes

  • One message per language for the button with the wordmark as a rich-text tag (word order kept, e.g. German
    "Mit Ever ID anmelden"); the project's next-intl type override now declares t.rich.
  • Strings for the new page in all 13 locales.
  • The architecture guard feature-surface-preserved counts the new route (134 route files, 180 handlers).
  • Review fixes: id_token_required wins over the e-mail claim (CodeRabbit), light-theme contrast on the verify page
    (Copilot), wordmark renderer hoisted (SonarCloud S6478).

How to Test This PR

  • Unit: yarn --cwd apps/web test (new: app/api/auth/ever-id/verify-email/route.test.ts; extended:
    o-auth.ever-id.test.ts, handoff.test.ts, log.test.ts, social-logins-buttons.test.tsx). Locally the full web
    suite passed at 194ee59 (134 suites, 1365 tests).
  • On stage (stage.ever.team, Ever ID on): the login and register pages show the full-width button; with an Ever ID
    whose e-mail is not verified the sign-in ends on "Verify your Ever ID e-mail" and the link opens Ever ID's verify form.

Screenshots

Added after the stage rollout (light + dark, narrow + wide).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a dedicated Ever ID login option with branded styling and accessible labels.
    • If your Ever ID email isn’t verified, you’ll see guidance to verify it, sign in again, or return to the app.
    • Added a secure redirect to Ever ID’s email-verification page, with localized guidance and actions available in 13 languages.