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) andUser.email_verified_at(datetime | None) — added in migrationb35f5bb68c11.EmailVerificationToken: uuid PK, uniquetoken, FKuser_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_tokenand_register_via_group. Sends the mail whenAUTH_ENABLE_EMAIL_CONFIRMATIONis on andbackground_tasksis 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 withraise_on_error=True.verify_email_service— validates token, invalidates all other tokens for that user, flipsemail_verified, redirects.request_public_verification_email_service— look-up-and-send behind the public endpoint; returns a uniformSuccessMessageregardless of outcome.assert_email_verification_grace_period(src/utils/api_utils.py) — raises HTTP 403 oncenow > 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 byLoggedUser.from_userand 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.htmlsrc/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, notuser.email). - FK cascade on
email_verification_token.user_idensures tokens are purged with the user.
Operational Considerations¶
- If
SENDGRID_API_KEYis 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_tokentable until the user is deleted. If this grows large, add a periodic cleanup job similar tocleanup_expired_refresh_tokens. - Grace-period lockouts can only be cleared by the user verifying, or by support setting
email_verified=Truedirectly in the DB.