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:
// Avoid
format!("http://app.com/verify?token={}", token)
// Prefer
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 inGradual 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 featuresChoose the flow that best matches your application's security and UX requirements.