Skip to content

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.

The password reset flow requires two main backend operations:

  1. Request Password Reset - Initiate password reset by sending email
  2. Reset Password - Complete password reset with token validation

Both operations must implement robust security measures including rate limiting, token management, and email verification.

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
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
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])
end
end
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])
end
end
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)
end
end
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)
end
end
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
"""
end
end
  • Maximum 5 attempts per email per hour
  • Sliding window implementation
  • Automatic cleanup of old attempts
  • Maximum 20 attempts per IP per hour
  • Protection against distributed attacks
  • Whitelist for trusted IPs (optional)
defp generate_secure_token do
:crypto.strong_rand_bytes(32)
|> Base.url_encode64(padding: false)
end
  • Store hashed tokens in database
  • Include expiration timestamp
  • Single-use tokens (mark as used)
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}
end
end
defp hash_password(password) do
Bcrypt.hash_pwd_salt(password, rounds: 12)
end
  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number
  • At least one special character
config/config.exs
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 configuration
config :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: :always
  • Use reliable email service (SendGrid, Mailgun, AWS SES)
  • Configure SPF, DKIM, and DMARC records
  • Monitor email delivery rates
  • Index on frequently queried columns
  • Implement cleanup job for expired tokens
  • Monitor query performance
  • Log all password reset attempts
  • Alert on unusual patterns
  • Track success/failure rates
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
end
end
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
end
end
  • Database migrations applied
  • Email service configured and tested
  • Environment variables set
  • Rate limiting configured
  • Monitoring and logging set up
  • Test password reset flow end-to-end
  • Verify email delivery
  • Check rate limiting functionality
  • Monitor error rates and performance
  • Validate security measures
  • Password reset request rates
  • Email delivery success rates
  • Token usage patterns
  • Failed authentication attempts
  • System performance metrics

The frontend expects the following GraphQL operations to be available:

mutation RequestPasswordReset($email: String!) {
requestPasswordReset(email: $email) {
success
message
}
}
mutation ResetPassword($token: String!, $password: String!, $confirmPassword: String!) {
resetPassword(token: $token, password: $password, confirmPassword: $confirmPassword) {
success
message
}
}

The frontend handles the following error scenarios:

  1. Network Errors - Connection failures, timeouts
  2. Validation Errors - Invalid email format, weak passwords
  3. Security Errors - Invalid tokens, expired tokens
  4. Rate Limiting - Too many requests (handled transparently)

The frontend can be configured with the following environment variables:

.env
VITE_GRAPHQL_ENDPOINT=http://localhost:4000/api/graphql
VITE_APP_NAME=Bartendie
  • Database schema and migrations
  • Basic GraphQL mutations
  • Token generation and validation
  • Password hashing and validation
  • Rate limiting implementation
  • Email enumeration protection
  • Security logging and monitoring
  • Token cleanup jobs
  • Email service configuration
  • Email templates
  • Delivery monitoring
  • Bounce handling
  • 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.