13.15. Email Verification
Table of Contents
Why Email Verification Matters
When a user signs up to your application, you usually want to confirm that:
- The email address actually exists.
- The person signing up controls that inbox.
- You can safely use that email for password resets, notifications, and security alerts.
This is what email verification solves.
Typical goals of email verification:
- Prevent fake or mistyped email addresses.
- Reduce spam and malicious signups.
- Protect account recovery flows that rely on email.
- Improve deliverability, since you send mail only to valid addresses.
In most systems, email verification is optional for basic browsing, but required for high risk actions like changing password, making payments, or accessing personal data.
Important: Never treat an unverified email as a trusted way to prove identity. Only a verified email can be used for password reset or other sensitive actions.
Common Email Verification Flow
Although implementations differ, most backends follow this basic flow:
- User registers
- User submits
email,password, and maybename. - Backend creates a user record with
is_email_verified = false. - Backend generates a verification token
- Token is random and unpredictable.
- Token is associated with the user and has an expiration time.
- Backend sends a verification email
- Email contains a unique link with the token in it, something like:
https://example.com/verify-email?token=XYZ - User clicks the link
- Browser sends a request to your backend with the token.
- Backend validates the token, checks expiry, and finds the user.
- Backend marks email as verified
- Set
is_email_verified = trueon the user. - Invalidate or delete the token so it cannot be reused.
- Optionally redirect to a “Email verified” page or auto log in.
- Optional: enforce verification checks
- Block login, or restrict actions, if
is_email_verifiedis false.
Here is a very high level sequence:
| Step | Actor | Action |
|---|---|---|
| 1 | Client | POST /register with email and password |
| 2 | Server | Create user, generate token, save token |
| 3 | Server | Send email with /verify-email?token=... |
| 4 | User | Clicks link in email |
| 5 | Client | Browser GET /verify-email?token=... |
| 6 | Server | Validate token, mark email verified, redirect/show page |
Verification Tokens
The core of email verification is the token. This token proves that whoever clicked the link had access to the email inbox.
What the Token Should Be
A verification token must be:
- Random: Hard to guess.
- Unique: Not reused across users or emails.
- Short lived: Expires after some time.
- Bound to a specific user: So you know which email to mark as verified.
There are two common styles:
- Opaque random token
- Example:
c31f5a8d6b854f92a1b7db9a69d9b33 - Stored in the database, usually hashed.
- Signed token (like a JWT or a signed string)
- Contains user info and an expiry timestamp.
- Backend can verify it without a database lookup for the token itself.
Random Token Example
In Python (conceptual example, not full code):
import secrets
def generate_verification_token() -> str:
return secrets.token_urlsafe(32) # e.g. 'aK8...'You could store it in a table like:
| Column | Type | Example |
|---|---|---|
| id | int (PK) | 1 |
| user_id | int (FK) | 42 |
| token_hash | text | sha256(token) |
| created_at | timestamp | 2026-08-27 10:00:00 |
| expires_at | timestamp | 2026-08-27 11:00:00 |
| used_at | timestamp | null or 2026-08-27 10:30:00 after verification |
You never store the token itself, only a hash of it:
import hashlib
def hash_token(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()
Rule: Treat email verification tokens like passwords.
Never log them, never expose them to analytics, and if you store them, store only a hash, not the raw token.
Signed Token Example
Instead of saving tokens in a table, you can sign the data and verify the signature when the user clicks:
- Token contains fields like:
user_idemailexp(expiration timestamp)- Token is signed with a secret key.
At verification time:
- Decode and verify the signature.
- Check expiry.
- Find the user and verify the email.
You still may want to store some state in your database (for example, when the user was verified), but you do not need a separate token table.
Designing the Database Fields
You need at least one field on your user table for verification status.
Typical user fields:
| Field | Type | Description |
|---|---|---|
| id | int (PK) | User ID |
| text | Unique email | |
| password_hash | text | Hashed password |
| is_email_verified | boolean | True if email is verified |
| email_verified_at | timestamp | When the email was verified |
Where to store tokens:
- Option A: Dedicated table
email_verification_tokens. - Option B: Store token info on the user itself (simpler, less flexible).
- Option C: Use signed tokens without a token table.
For beginners, a dedicated table is easy to understand and secure.
Generating and Sending Verification Emails
When to Send the Email
You usually send a verification email:
- Immediately after registration.
- When a user changes their email address.
- When a user requests that you resend the verification email.
Each of these flows should:
- Generate a new token.
- Invalidate or mark previous tokens related to the same email.
- Send a fresh email with the new link.
What the Email Should Contain
A typical verification email includes:
- A friendly greeting: “Hi Alice,”
- A short explanation: “Please confirm your email address.”
- The verification link: A button or link to your frontend or backend URL.
- Expiration info: “This link will expire in 60 minutes.”
- A hint if they did not sign up: “If you did not create an account, ignore this email.”
Example text:
Subject: Confirm your email for ExampleApp
Hi Alice,
Thank you for creating an account on ExampleApp.
Please confirm your email by clicking this link:
https://example.com/verify-email?token=XYZ
This link will expire in 60 minutes.
If you did not create this account, you can safely ignore this email.
Constructing the Verification Link
The link usually contains the token as:
- A query parameter:
https://example.com/verify-email?token=XYZ
Or
- A path parameter:
https://example.com/verify-email/XYZ
Query parameter style is more common and easier to copy and paste.
Backend route examples:
GET /verify-email?token=XYZGET /api/auth/verify-email?token=XYZ
In a real app, that route might:
- Call your API.
- Show a confirmation page in the browser.
- Or redirect to a SPA (single page app) that displays the result.
Important: Never put sensitive data such as passwords or password hashes into the verification link. The link should contain only the token and maybe non sensitive data like a redirect URL.
Verifying the Token
When a request arrives at your verification endpoint:
- Read the token from the request.
- From query:
token=.... - From path:
/verify-email/{token}. - Validate the token format.
- If the token is missing or too short, return an error.
- Look up the token.
- If using a token table, hash the token and search for the hash.
- If using a signed token, verify the signature and parse its data.
- Check token state:
- Not expired.
- Not already used.
- Matches the right user and email.
- Mark user as verified:
- Set
is_email_verified = true. - Set
email_verified_at = now. - Optionally set
used_at = nowon the token or delete the token. - Respond to the client:
- Redirect to a success page.
- Or return a JSON response like
{ "detail": "Email verified" }.
Example JSON responses:
- Success:
{
"detail": "Email successfully verified."
}- Token invalid:
{
"detail": "Invalid or already used verification token."
}- Token expired:
{
"detail": "Verification link has expired. Please request a new one."
}You can use different HTTP status codes:
| Situation | Status code |
|---|---|
| Successful verification | 200 or 302 |
| Missing or invalid token | 400 |
| Expired or used token | 400 or 410 |
| Internal server error | 500 |
Token Expiration and Resending
Choosing an Expiration Time
If the token never expires, anyone who gains access to the email at any time can verify the account. Also, old links floating around are not great for security.
Common expiration choices:
| Expiration | When it makes sense |
|---|---|
| 15 minutes | Highly sensitive apps, banking, security heavy |
| 1 hour | Most typical web apps |
| 24 hours | Very low friction needed, less strict |
Rule: Email verification tokens must have an expiration time.
A common choice is 1 hour.
Resending the Verification Email
You must allow users to ask for a new verification email, especially if:
- They never received the first one.
- The token expired.
- They deleted the email.
A typical endpoint might be:
POST /resend-verification
Behavior:
- Check if the user is already verified.
- If yes, respond with
"Email already verified."and do nothing. - Generate a new token.
- Invalidate or mark older verification tokens as used or expired.
- Send a new email.
To avoid abuse:
- Rate limit how often the user can request a new email.
For example, one email every 5 or 10 minutes. - Log attempts for monitoring suspicious patterns.
Returning a neutral response is often better:
"If there is an unverified account with this email, a new verification email has been sent."
This avoids revealing whether an email is registered.
Linking Email Verification With Registration
Typically, the backend does this directly after a successful registration:
- Create the user with
is_email_verified = false. - Generate a token.
- Send a verification email.
Example pseudo-steps:
- Endpoint:
POST /register - Input:
email,password, etc. - Workflow:
- Validate inputs.
- Create user record.
- Generate verification token for
user.id. - Enqueue a background job to send the email.
- Return a response, for example:
{
"detail": "Registration successful. Please check your email to verify your account."
}
Login should then check is_email_verified:
- If unverified, you might:
- Block login completely, or
- Allow login but restrict certain actions and keep reminding user to verify.
You must design this based on your product requirements.
Security Considerations
Email verification looks simple, but there are common security pitfalls.
1. Token Strength
Tokens must be sufficiently random and long. Using secrets.token_urlsafe(32) or equivalent is a safe baseline.
Avoid:
- Short tokens like 6 digits, unless you combine them with extra protection and rate limiting.
- Tokens derived from predictable data like
user_idand timestamps without a secure signature.
2. Token Storage
Storing tokens in plain text is risky:
- If your database is leaked, attackers can verify other users’ emails.
- Even if limited, this is unnecessary risk.
Better approach:
- Store a hash of the token, similar to how passwords are handled.
- On verification, hash the provided token and compare.
3. One Time Use
A verification link should be usable once.
After a successful verification:
- Delete the token.
- Or set
used_atand check thatused_at IS NULLon further attempts.
Even if someone copies the link from an old email, it should not work again after it has been used.
4. No Account Enumeration
Your responses should not reveal whether an email is registered.
Cases:
- Registration: “If this email is not already used, we will create an account and send a verification email.”
- Resend verification: “If there is an unverified account with this email, a verification email has been sent.”
This prevents attackers from using your API as an “email check” service.
5. Protecting the Verification Endpoint
Even though the endpoint is not as sensitive as login, you still need:
- Rate limiting: To stop automated attempts to guess tokens.
- Logging: To detect unusual spikes and possible abuse.
- TLS (HTTPS): So tokens are not visible in transit.
Important: Always use HTTPS on verification links.
Sending a token over plain HTTP lets attackers on the network intercept and hijack accounts.
User Experience Considerations
Good UX around email verification reduces user frustration and support tickets.
Clear Messages
On registration:
- Tell users clearly that they must verify their email.
- Show the address where the email was sent.
- Mention that it may take a few minutes to arrive.
- Suggest checking spam or promotions folders.
On verification pages:
- Success: “Your email is confirmed. You can now log in.”
- Already verified: “This email was already verified. You can log in.”
- Expired: “This link has expired. Request a new verification email.”
Automatic Login After Verification
Some apps automatically log the user in after the email is verified. Others ask for login again.
Options:
- Auto login: Use a short lived token in the same link to create a session.
- No auto login: Redirect to login page with a success message.
Both are valid; pick what matches your security and UX goals.
Mobile and Email Clients
Consider that:
- Some users click links on mobile devices.
- Some email clients break long URLs or add tracking parameters.
- Some email clients prefetch links, which can trigger the verification before the user clicks.
To mitigate prefetch issues:
- Use verification pages that still ask the user to confirm in the browser.
- Or design your workflow so that prefetching does not finalize the verification without some user action.
Handling Edge Cases
Real systems must handle scenarios that are not part of the “happy path”.
User Changes Email Address
When a user changes their email:
- Keep
is_email_verified = falsefor the new email. - Clear
email_verified_at. - Generate a new verification token.
- Send a new verification email to the new address.
You might also want to:
- Notify the old email that the change happened.
- Allow a grace period where some features still work.
Multiple Verification Attempts
What if the user clicks the same link twice?
- First time: Success, mark email verified.
- Second time: Show a friendly message like, “This email is already verified.”
Do not treat it as an error to avoid confusing users.
Token Stolen
If you suspect a token might be compromised:
- Invalidate all existing tokens for that user.
- Require them to request a new verification email.
- If they already used it, the attacker cannot use it again because it is one time use.
Putting It All Together: Example Flow
Here is a simple end to end example of how the main endpoints could interact, in a backend agnostic way.
1. Register
- Endpoint:
POST /register - Request body:
{
"email": "alice@example.com",
"password": "supersecret"
}- Backend:
- Create user with
is_email_verified = false. - Generate token
T. - Save
hash(T)inemail_verification_tokens. - Send email to
alice@example.comwith:
https://app.example.com/verify-email?token=T
- Response:
{
"detail": "Registration successful. Please check your email to verify your account."
}2. Verify Email
- Endpoint:
GET /verify-email?token=T - Backend:
- Read token
T. - Compute
hash(T)and look up token. - Check not expired, not used.
- Mark user’s
is_email_verified = true,email_verified_at = now. - Mark token as used.
- Redirect to frontend or return success JSON.
- Response (JSON case):
{
"detail": "Email successfully verified."
}3. Resend Verification
- Endpoint:
POST /resend-verification - Request body:
{
"email": "alice@example.com"
}- Backend:
- Find user by email.
- If user not found: still respond success message.
- If
is_email_verifiedis true: respond neutral success message. - If not verified:
- Invalidate old tokens.
- Generate new token and send new email.
- Enforce rate limiting.
- Response:
{
"detail": "If an unverified account exists with this email, a new verification email has been sent."
}Summary
Email verification connects your registration system with a real user identity channel, the email inbox. A solid implementation:
- Creates users as unverified at registration time.
- Generates secure, short lived, one time tokens.
- Sends a clear verification email with a tokenized link.
- Verifies and invalidates tokens securely when the user clicks.
- Allows safe resending without leaking which emails exist.
- Integrates neatly with login and account management flows.
By designing tokens, storage, and flows carefully, you avoid common pitfalls and create a safer, more reliable backend for your users.
Views: 10
KAHIBARO