Password Reset Flow Backend Integration Guide
Password Reset Flow Backend Integration Guide
Section titled “Password Reset Flow Backend Integration Guide”This document provides comprehensive specifications for implementing the backend components required to support the password reset flow in Bartendie, including GraphQL schema definitions, security requirements, and implementation guidelines.
Overview
Section titled “Overview”The password reset flow requires two main backend operations:
- Request Password Reset - Initiate password reset by sending email
- Reset Password - Complete password reset with token validation
Both operations must implement robust security measures including rate limiting, token management, and email verification.
GraphQL Schema Requirements
Section titled “GraphQL Schema Requirements”Mutations
Section titled “Mutations”Request Password Reset
Section titled “Request Password Reset”type Mutation { requestPasswordReset(email: String!): PasswordResetResponse!}
type PasswordResetResponse { success: Boolean! message: String}Input Validation:
- Email format validation (RFC 5322 compliant)
- Email length limits (max 254 characters)
- Sanitization against injection attacks
Security Considerations:
- Always return success to prevent email enumeration
- Rate limiting per IP address and email
- Log all attempts for security monitoring
Reset Password
Section titled “Reset Password”type Mutation { resetPassword( token: String! password: String! confirmPassword: String! ): PasswordResetResponse!}Input Validation:
- Token format validation (UUID or secure random string)
- Password strength requirements (minimum 8 characters, complexity rules)
- Password confirmation matching
- Token expiration validation
Elixir/Phoenix Implementation
Section titled “Elixir/Phoenix Implementation”Database Schema
Section titled “Database Schema”Password Reset Tokens Table
Section titled “Password Reset Tokens Table”defmodule Bartendie.Repo.Migrations.CreatePasswordResetTokens do use Ecto.Migration
def change do create table(:password_reset_tokens, primary_key: false) do add :id, :binary_id, primary_key: true add :token, :string, null: false add :user_id, references(:users, on_delete: :delete_all, type: :binary_id), null: false add :expires_at, :utc_datetime, null: false add :used_at, :utc_datetime add :created_at, :utc_datetime, null: false end
create unique_index(:password_reset_tokens, [:token]) create index(:password_reset_tokens, [:user_id]) create index(:password_reset_tokens, [:expires_at]) endendRate Limiting Table
Section titled “Rate Limiting Table”defmodule Bartendie.Repo.Migrations.CreatePasswordResetAttempts do use Ecto.Migration
def change do create table(:password_reset_attempts, primary_key: false) do add :id, :binary_id, primary_key: true add :email, :string, null: false add :ip_address, :string, null: false add :attempted_at, :utc_datetime, null: false end
create index(:password_reset_attempts, [:email, :attempted_at]) create index(:password_reset_attempts, [:ip_address, :attempted_at]) endendContext Module
Section titled “Context Module”defmodule Bartendie.Accounts.PasswordReset do @moduledoc """ Context for password reset operations """
import Ecto.Query alias Bartendie.Repo alias Bartendie.Accounts.{User, PasswordResetToken, PasswordResetAttempt} alias Bartendie.Email.PasswordResetEmail
@token_expiry_hours 1 @max_attempts_per_hour 5 @max_attempts_per_ip_per_hour 20
def request_password_reset(email, ip_address) do with :ok <- check_rate_limits(email, ip_address), {:ok, user} <- get_user_by_email(email), {:ok, token} <- create_reset_token(user), :ok <- send_reset_email(user, token) do log_attempt(email, ip_address) {:ok, %{success: true, message: "Reset email sent"}} else {:error, :rate_limited} -> {:ok, %{success: true, message: "Reset email sent"}} # Security: don't reveal rate limiting
{:error, :user_not_found} -> log_attempt(email, ip_address) {:ok, %{success: true, message: "Reset email sent"}} # Security: don't reveal user existence
{:error, reason} -> {:error, %{success: false, message: "Failed to send reset email"}} end end
def reset_password(token, password, confirm_password) do with :ok <- validate_passwords(password, confirm_password), {:ok, reset_token} <- get_valid_token(token), {:ok, user} <- get_user(reset_token.user_id), {:ok, _user} <- update_password(user, password), :ok <- mark_token_used(reset_token) do {:ok, %{success: true, message: "Password reset successful"}} else {:error, :invalid_token} -> {:error, %{success: false, message: "Invalid or expired reset token"}}
{:error, :weak_password} -> {:error, %{success: false, message: "Password does not meet security requirements"}}
{:error, :password_mismatch} -> {:error, %{success: false, message: "Passwords do not match"}}
{:error, _reason} -> {:error, %{success: false, message: "Failed to reset password"}} end end
# Private functions
defp check_rate_limits(email, ip_address) do email_attempts = count_recent_attempts_by_email(email) ip_attempts = count_recent_attempts_by_ip(ip_address)
cond do email_attempts >= @max_attempts_per_hour -> {:error, :rate_limited}
ip_attempts >= @max_attempts_per_ip_per_hour -> {:error, :rate_limited}
true -> :ok end end
defp create_reset_token(user) do # Invalidate existing tokens invalidate_existing_tokens(user.id)
token_attrs = %{ token: generate_secure_token(), user_id: user.id, expires_at: DateTime.add(DateTime.utc_now(), @token_expiry_hours, :hour), created_at: DateTime.utc_now() }
%PasswordResetToken{} |> PasswordResetToken.changeset(token_attrs) |> Repo.insert() end
defp generate_secure_token do :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) end
defp validate_passwords(password, confirm_password) do cond do password != confirm_password -> {:error, :password_mismatch}
not strong_password?(password) -> {:error, :weak_password}
true -> :ok end end
defp strong_password?(password) do String.length(password) >= 8 and Regex.match?(~r/[A-Z]/, password) and Regex.match?(~r/[a-z]/, password) and Regex.match?(~r/[0-9]/, password) and Regex.match?(~r/[!@#$%^&*(),.?":{}|<>]/, password) endendGraphQL Resolvers
Section titled “GraphQL Resolvers”defmodule BartendieWeb.Resolvers.PasswordReset do alias Bartendie.Accounts.PasswordReset
def request_password_reset(_parent, %{email: email}, %{context: %{ip_address: ip}}) do PasswordReset.request_password_reset(email, ip) end
def reset_password(_parent, %{token: token, password: password, confirm_password: confirm_password}, _context) do PasswordReset.reset_password(token, password, confirm_password) endendEmail Service Integration
Section titled “Email Service Integration”Email Template
Section titled “Email Template”defmodule Bartendie.Email.PasswordResetEmail do use Phoenix.Swoosh, template_root: "lib/bartendie_web/templates", template_path: "email"
def password_reset_email(user, token) do reset_url = build_reset_url(token)
new() |> to({user.name || "User", user.email}) |> from({"Bartendie", "noreply@bartendie.com"}) |> subject("Reset your Bartendie password") |> html_body(render_html_template(user, reset_url)) |> text_body(render_text_template(user, reset_url)) end
defp build_reset_url(token) do base_url = Application.get_env(:bartendie, :frontend_url) "#{base_url}/reset-password?token=#{token}" end
defp render_html_template(user, reset_url) do """ <h2>Reset Your Password</h2> <p>Hello #{user.name || "there"},</p> <p>You requested to reset your password for your Bartendie account.</p> <p>Click the link below to reset your password:</p> <p><a href="#{reset_url}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Reset Password</a></p> <p>This link will expire in 1 hour for security reasons.</p> <p>If you didn't request this password reset, please ignore this email.</p> <p>Best regards,<br>The Bartendie Team</p> """ end
defp render_text_template(user, reset_url) do """ Reset Your Password
Hello #{user.name || "there"},
You requested to reset your password for your Bartendie account.
Click the link below to reset your password: #{reset_url}
This link will expire in 1 hour for security reasons.
If you didn't request this password reset, please ignore this email.
Best regards, The Bartendie Team """ endendSecurity Implementation
Section titled “Security Implementation”Rate Limiting
Section titled “Rate Limiting”Per-Email Limits
Section titled “Per-Email Limits”- Maximum 5 attempts per email per hour
- Sliding window implementation
- Automatic cleanup of old attempts
Per-IP Limits
Section titled “Per-IP Limits”- Maximum 20 attempts per IP per hour
- Protection against distributed attacks
- Whitelist for trusted IPs (optional)
Token Security
Section titled “Token Security”Token Generation
Section titled “Token Generation”defp generate_secure_token do :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)endToken Storage
Section titled “Token Storage”- Store hashed tokens in database
- Include expiration timestamp
- Single-use tokens (mark as used)
Token Validation
Section titled “Token Validation”defp get_valid_token(token) do now = DateTime.utc_now()
from(t in PasswordResetToken, where: t.token == ^token and t.expires_at > ^now and is_nil(t.used_at) ) |> Repo.one() |> case do nil -> {:error, :invalid_token} token -> {:ok, token} endendPassword Security
Section titled “Password Security”Hashing
Section titled “Hashing”defp hash_password(password) do Bcrypt.hash_pwd_salt(password, rounds: 12)endValidation Rules
Section titled “Validation Rules”- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”config :bartendie, frontend_url: System.get_env("FRONTEND_URL", "http://localhost:3000"), password_reset_token_expiry_hours: 1, max_password_reset_attempts_per_hour: 5, max_password_reset_attempts_per_ip_per_hour: 20
# Email configurationconfig :bartendie, Bartendie.Mailer, adapter: Swoosh.Adapters.SMTP, relay: System.get_env("SMTP_RELAY"), username: System.get_env("SMTP_USERNAME"), password: System.get_env("SMTP_PASSWORD"), port: 587, auth: :always, tls: :alwaysProduction Considerations
Section titled “Production Considerations”Email Service
Section titled “Email Service”- Use reliable email service (SendGrid, Mailgun, AWS SES)
- Configure SPF, DKIM, and DMARC records
- Monitor email delivery rates
Database Performance
Section titled “Database Performance”- Index on frequently queried columns
- Implement cleanup job for expired tokens
- Monitor query performance
Monitoring
Section titled “Monitoring”- Log all password reset attempts
- Alert on unusual patterns
- Track success/failure rates
Testing Strategy
Section titled “Testing Strategy”Unit Tests
Section titled “Unit Tests”defmodule Bartendie.Accounts.PasswordResetTest do use Bartendie.DataCase alias Bartendie.Accounts.PasswordReset
describe "request_password_reset/2" do test "creates token for valid user" do user = insert(:user)
assert {:ok, %{success: true}} = PasswordReset.request_password_reset(user.email, "127.0.0.1") end
test "returns success for non-existent user (security)" do assert {:ok, %{success: true}} = PasswordReset.request_password_reset("nonexistent@example.com", "127.0.0.1") end
test "enforces rate limiting" do user = insert(:user)
# Exceed rate limit for _ <- 1..6 do PasswordReset.request_password_reset(user.email, "127.0.0.1") end
# Should still return success (security) assert {:ok, %{success: true}} = PasswordReset.request_password_reset(user.email, "127.0.0.1") end end
describe "reset_password/3" do test "resets password with valid token" do user = insert(:user) token = insert(:password_reset_token, user: user)
assert {:ok, %{success: true}} = PasswordReset.reset_password(token.token, "NewPassword123!", "NewPassword123!") end
test "rejects expired token" do user = insert(:user) token = insert(:password_reset_token, user: user, expires_at: DateTime.add(DateTime.utc_now(), -1, :hour))
assert {:error, %{success: false}} = PasswordReset.reset_password(token.token, "NewPassword123!", "NewPassword123!") end endendIntegration Tests
Section titled “Integration Tests”defmodule BartendieWeb.PasswordResetIntegrationTest do use BartendieWeb.ConnCase
describe "password reset flow" do test "complete password reset flow", %{conn: conn} do user = insert(:user)
# Request password reset mutation = """ mutation { requestPasswordReset(email: "#{user.email}") { success message } } """
conn = post(conn, "/api/graphql", %{query: mutation}) assert %{"success" => true} = json_response(conn, 200)["data"]["requestPasswordReset"]
# Get token from database token = Repo.get_by(PasswordResetToken, user_id: user.id)
# Reset password reset_mutation = """ mutation { resetPassword( token: "#{token.token}" password: "NewPassword123!" confirmPassword: "NewPassword123!" ) { success message } } """
conn = post(conn, "/api/graphql", %{query: reset_mutation}) assert %{"success" => true} = json_response(conn, 200)["data"]["resetPassword"] end endendDeployment Checklist
Section titled “Deployment Checklist”Pre-Deployment
Section titled “Pre-Deployment”- Database migrations applied
- Email service configured and tested
- Environment variables set
- Rate limiting configured
- Monitoring and logging set up
Post-Deployment
Section titled “Post-Deployment”- Test password reset flow end-to-end
- Verify email delivery
- Check rate limiting functionality
- Monitor error rates and performance
- Validate security measures
Monitoring
Section titled “Monitoring”- Password reset request rates
- Email delivery success rates
- Token usage patterns
- Failed authentication attempts
- System performance metrics
API Contract
Section titled “API Contract”Frontend-Backend Interface
Section titled “Frontend-Backend Interface”The frontend expects the following GraphQL operations to be available:
Request Password Reset
Section titled “Request Password Reset”mutation RequestPasswordReset($email: String!) { requestPasswordReset(email: $email) { success message }}Reset Password
Section titled “Reset Password”mutation ResetPassword($token: String!, $password: String!, $confirmPassword: String!) { resetPassword(token: $token, password: $password, confirmPassword: $confirmPassword) { success message }}Error Handling Contract
Section titled “Error Handling Contract”The frontend handles the following error scenarios:
- Network Errors - Connection failures, timeouts
- Validation Errors - Invalid email format, weak passwords
- Security Errors - Invalid tokens, expired tokens
- Rate Limiting - Too many requests (handled transparently)
Frontend Configuration
Section titled “Frontend Configuration”The frontend can be configured with the following environment variables:
VITE_GRAPHQL_ENDPOINT=http://localhost:4000/api/graphqlVITE_APP_NAME=BartendieImplementation Timeline
Section titled “Implementation Timeline”Phase 1: Core Backend (Week 1)
Section titled “Phase 1: Core Backend (Week 1)”- Database schema and migrations
- Basic GraphQL mutations
- Token generation and validation
- Password hashing and validation
Phase 2: Security Features (Week 2)
Section titled “Phase 2: Security Features (Week 2)”- Rate limiting implementation
- Email enumeration protection
- Security logging and monitoring
- Token cleanup jobs
Phase 3: Email Integration (Week 3)
Section titled “Phase 3: Email Integration (Week 3)”- Email service configuration
- Email templates
- Delivery monitoring
- Bounce handling
Phase 4: Testing and Deployment (Week 4)
Section titled “Phase 4: Testing and Deployment (Week 4)”- Comprehensive test suite
- Load testing
- Security audit
- Production deployment
This comprehensive backend integration guide ensures secure, scalable, and reliable password reset functionality for the Bartendie application.