Skip to content

Email Verification Architecture

Overview

Users are created with email_verified=False. They can log in during a grace period (EMAIL_VERIFICATION_GRACE_PERIOD_DAYS, default 3 days, counted from User.joined_on). After the grace period, login is blocked with HTTP 403 until the email is verified.


Configuration

All settings are defined in src/config/auth_settings.py::AuthSettings and re-exported by src/config/config.py.

Variable Default Purpose
AUTH_ENABLE_EMAIL_CONFIRMATION True Master switch. When off, new users are created as verified and no mail is sent.
EMAIL_VERIFICATION_TOKEN_EXPIRE_MINUTES 1440 (24 h) Single-verification-link validity.
EMAIL_VERIFICATION_GRACE_PERIOD_DAYS 3 Grace window for logging in while unverified.
VERIFY_EMAIL_LINK .../api/v1/auth/verify-email/ Base URL embedded in the email. Token is appended.
EMAIL_VERIFICATION_REDIRECT_LINK .../email-verified Frontend success page after clicking the link.

Data Model

  • User.email_verified (bool) and User.email_verified_at (datetime | None) — added in migration b35f5bb68c11.
  • EmailVerificationToken: uuid PK, unique token, FK user_id (CASCADE) with index, email, expires_at, used.

Lifecycle

flowchart TD
    A[Register] --> B[User created with email_verified=False]
    B --> C[Token generated, link emailed in background]
    C --> D[Immediate sign-in with email_verified=False in response]
    D --> E[UI renders persistent verification banner]
    E --> F{User clicks link}
    F --> G["GET /api/v1/auth/verify-email/{token}"]
    G --> H[Token marked used, other tokens invalidated]
    H --> I[email_verified=True, email_verified_at=now]
    I --> J["302 redirect to EMAIL_VERIFICATION_REDIRECT_LINK"]

Login paths enforce the grace period via src/utils/api_utils.py::assert_email_verification_grace_period(user), called from authenticate_user, authenticate_user_by_firebase_token, and verify_magic_token_service.


Endpoints

Method Path Auth Purpose
GET /api/v1/auth/verify-email/{token} None Consume a verification token (link in email)
POST /api/v1/auth/resend-verification-email Bearer Logged-in user requests a new link
POST /api/v1/auth/request-verification-email None, rate-limited 3/hour per IP Locked-out user requests a new link. Uniform response to prevent email enumeration.

Per-Registration-Path Behaviour

Entry point Creates user as Sends verification email
POST /register (password or Firebase) email_verified = decoded_token.email_verified for Firebase, else False Yes (unless Firebase reports verified)
POST /register/passwordless then GET /register/confirm/{token} False Yes
POST /register/direct (admin-provisioned) True — vouched for by the admin No

Key Functions

  • _handle_email_verification_for_new_user (src/services/auth.py) — shared helper called from _register_via_token and _register_via_group. Sends the mail when AUTH_ENABLE_EMAIL_CONFIRMATION is on and background_tasks is available; otherwise flips the user to verified.
  • send_verification_email(..., raise_on_error=False) — by default swallows exceptions so registration stays non-blocking; the resend endpoint calls it with raise_on_error=True.
  • verify_email_service — validates token, invalidates all other tokens for that user, flips email_verified, redirects.
  • request_public_verification_email_service — look-up-and-send behind the public endpoint; returns a uniform SuccessMessage regardless of outcome.
  • assert_email_verification_grace_period (src/utils/api_utils.py) — raises HTTP 403 once now > user.joined_on + grace_period_days. No-op when email confirmation is disabled or the user is already verified.

Client-Visible Flags

email_verified is exposed on:

  • UserCreateResponse — register response body.
  • LoggedUser — populated by LoggedUser.from_user and propagated through every authenticated request.

Frontend uses this to render the verification banner and detect the verified state after the user returns from the link.


Email Templates

Branded copies live at:

  • src/templates/email_templates/primethink/email_verification.html
  • src/templates/email_templates/assist/email_verification.html

email_env selects the folder via EMAIL_TEMPLATES_FOLDER (default primethink). Template variables: first_name, verification_link, link_expiry_hours, current_year, plus globals BASE_UI_URL, UI_GET_CHAT.


Security Notes

  • Tokens are generated with generate_urlsafe_token() (cryptographically secure) and stored as unique strings.
  • Tokens are single-use; verifying one invalidates all other outstanding tokens for the same user.
  • Resend endpoints invalidate prior unused tokens before issuing a new one.
  • The public resend endpoint returns identical response shape and timing regardless of whether the email is registered, already verified, or errored — to prevent email enumeration.
  • Logs intentionally avoid PII (user.id, not user.email).
  • FK cascade on email_verification_token.user_id ensures tokens are purged with the user.

Operational Considerations

  • If SENDGRID_API_KEY is misconfigured, registration still succeeds (exception is logged and swallowed). Users can request a resend. The authenticated resend endpoint surfaces errors as HTTP 500.
  • Expired tokens remain in the email_verification_token table until the user is deleted. If this grows large, add a periodic cleanup job similar to cleanup_expired_refresh_tokens.
  • Grace-period lockouts can only be cleared by the user verifying, or by support setting email_verified=True directly in the DB.