Email Verification
Understanding email verification flows in rs-auth
Email Verification
rs-auth provides flexible email verification flows that can be configured based on your application's security requirements.
Default Behavior
By default, rs-auth follows a user-friendly approach:
Signup Flow
- User submits signup form
- Account is created with
email_verified = false - User is automatically signed in
- Verification email is sent
- User can use the app immediately
This approach prioritizes user experience while still encouraging email verification.
Why Auto Sign-In?
Auto sign-in after signup provides:
- Better UX: Users can start using the app immediately
- Higher conversion: No friction between signup and first use
- Gradual verification: Email verification happens in the background
This is the same approach used by modern apps like GitHub, Notion, and Linear.
Verification Token Flow
When a user signs up, rs-auth:
1. Generates a Verification Token
let token = generate_verification_token(); // 32 random bytes
let token_hash = sha256(&token);The token is:
- Cryptographically random
- Hashed before database storage
- Time-limited (default: 24 hours)
2. Stores the Token
INSERT INTO email_verification_tokens (
user_id,
token_hash,
expires_at
) VALUES ($1, $2, NOW() + INTERVAL '24 hours')3. Sends Verification Email
send_verification_email(
user.email,
format!("https://yourapp.com/verify?token={}", token)
).await?;The email contains a link with the verification token.
4. User Clicks Link
When the user clicks the verification link:
// POST /auth/verify-email
{
"token": "verification-token-here"
}rs-auth:
- Hashes the provided token
- Looks up the token in the database
- Checks if it's expired
- Marks the user's email as verified
- Deletes the token
Enforcing Email Verification
For applications that require verified emails, you can enforce verification:
Configuration
let auth_config = AuthConfig::builder()
.require_email_verification(true)
.build();With this setting:
- Signup still creates the account
- User is not automatically signed in
- Login is blocked until email is verified
Login Behavior
// POST /auth/login
{
"email": "user@example.com",
"password": "password123"
}
// Response if email not verified
{
"error": "Email not verified",
"code": "EMAIL_NOT_VERIFIED"
}Verification Required Middleware
You can also enforce verification at the route level:
use rs_auth::middleware::require_verified_email;
let app = Router::new()
.route("/api/sensitive", post(sensitive_handler))
.layer(middleware::from_fn(require_verified_email));This allows:
- Users to sign in
- Access to some routes
- Blocking sensitive routes until verified
Resending Verification Emails
Users can request a new verification email:
// POST /auth/resend-verification
{
"email": "user@example.com"
}rs-auth will:
- Check if the email exists
- Check if already verified
- Generate a new token
- Invalidate old tokens
- Send a new email
Rate Limiting
To prevent abuse, implement rate limiting:
use tower::limit::RateLimitLayer;
let app = Router::new()
.route("/auth/resend-verification", post(resend_handler))
.layer(RateLimitLayer::new(5, Duration::minutes(15)));This limits users to 5 resend requests per 15 minutes.
Token Security
Token Generation
Verification tokens are generated securely:
use rand::Rng;
fn generate_verification_token() -> String {
let mut rng = rand::thread_rng();
let bytes: [u8; 32] = rng.gen();
base64::encode(bytes)
}Token Hashing
Tokens are hashed before storage:
use sha2::{Sha256, Digest};
fn hash_token(token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
format!("{:x}", hasher.finalize())
}This means:
- Database compromise doesn't expose valid tokens
- Tokens can't be reverse-engineered
- Each token is single-use
Token Expiration
Tokens expire after a configurable duration:
let auth_config = AuthConfig::builder()
.verification_token_duration(Duration::hours(24))
.build();Expired tokens are:
- Rejected on verification
- Cleaned up periodically
Email Configuration
rs-auth supports multiple email providers:
SMTP
use rs_auth::email::SmtpConfig;
let email_config = SmtpConfig::builder()
.host("smtp.gmail.com")
.port(587)
.username("your-email@gmail.com")
.password(&std::env::var("SMTP_PASSWORD")?)
.from("noreply@yourapp.com")
.build();Resend
use rs_auth::email::ResendConfig;
let email_config = ResendConfig::builder()
.api_key(&std::env::var("RESEND_API_KEY")?)
.from("noreply@yourapp.com")
.build();Custom Provider
Implement the EmailProvider trait:
use rs_auth::email::EmailProvider;
struct CustomEmailProvider {
// Your provider config
}
#[async_trait]
impl EmailProvider for CustomEmailProvider {
async fn send_email(
&self,
to: &str,
subject: &str,
body: &str,
) -> Result<(), EmailError> {
// Your implementation
}
}Customizing Email Templates
Override the default verification email template:
let auth_config = AuthConfig::builder()
.verification_email_template(|token, user| {
format!(
"Hi {},\n\nClick here to verify: https://app.com/verify?token={}",
user.name, token
)
})
.build();For HTML emails:
.verification_email_template(|token, user| {
format!(r#"
<!DOCTYPE html>
<html>
<body>
<h1>Welcome, {}!</h1>
<p>Click the button below to verify your email:</p>
<a href="https://app.com/verify?token={}"
style="background: #000; color: #fff; padding: 12px 24px;">
Verify Email
</a>
</body>
</html>
"#, user.name, token)
})Verification Status
Check if a user's email is verified:
use rs_auth::extractors::Session;
async fn check_verification(session: Session) -> Json<Status> {
let user = get_user(session.user_id).await?;
Json(Status {
verified: user.email_verified,
})
}Best Practices
1. Use HTTPS
Always use HTTPS for verification links:
// ❌ Bad
format!("http://app.com/verify?token={}", token)
// ✅ Good
format!("https://app.com/verify?token={}", token)2. Short Token Lifetime
Use short-lived tokens (24 hours or less):
.verification_token_duration(Duration::hours(24))3. Single-Use Tokens
Always delete tokens after use:
// After successful verification
DELETE FROM email_verification_tokens WHERE token_hash = $14. Clear Error Messages
Provide helpful error messages:
match verify_result {
Err(VerificationError::TokenExpired) => {
"Token expired. Request a new verification email."
}
Err(VerificationError::TokenNotFound) => {
"Invalid token. Check your email for the correct link."
}
Err(VerificationError::AlreadyVerified) => {
"Email already verified. You can log in."
}
Ok(_) => "Email verified successfully!"
}5. Graceful Degradation
Allow users to use the app while unverified, but:
// Show banner for unverified users
if !user.email_verified {
show_verification_banner();
}
// Block sensitive actions
if !user.email_verified && is_sensitive_action {
return Err("Please verify your email first");
}Verification Flow Examples
Immediate Access (Default)
1. User signs up
2. Account created (email_verified = false)
3. User automatically signed in ✅
4. Verification email sent
5. User can use app
6. User clicks verification link (optional)
7. Email marked as verifiedVerification Required
1. User signs up
2. Account created (email_verified = false)
3. Verification email sent
4. User tries to log in ❌
5. Login blocked: "Please verify your email"
6. User clicks verification link
7. Email marked as verified
8. User can now log in ✅Gradual Enforcement
1. User signs up
2. Account created (email_verified = false)
3. User automatically signed in ✅
4. User can access basic features
5. User tries sensitive action ❌
6. Blocked: "Verify email to continue"
7. User clicks verification link
8. Email marked as verified
9. User can access all features ✅Choose the flow that best matches your application's security and UX requirements.