Skip to main content

Overview

The password reset flow consists of two endpoints:
  1. Forgot Password - Initiates password reset and sends verification code via email
  2. Reset Password - Completes password reset with verification code and new password
This flow uses AWS Cognito’s built-in password reset mechanism with email delivery. Password Reset Flow:
  1. User requests password reset with email
  2. Cognito generates 6-digit verification code
  3. Code sent to user’s email (via Cognito email templates)
  4. User submits code + new password
  5. Cognito validates and updates password
  6. User can login with new password

Step 1: Forgot Password

POST /api/auth/forgot-password

Initiates password reset process by sending verification code to user’s email.

Request

username
string
required
User’s email address. The API automatically looks up the Cognito username (UUID) from email.Example: [email protected]

Request Example

{
  "username": "[email protected]"
}

Response

Success Response (200 OK)

success
boolean
required
Always true for successful requests.
message
string
required
Instructions for next steps.Value: "Reset instructions sent to email. Please check your inbox for the verification code."
delivery
object
required
Cognito delivery details for verification code.

Success Response Example

{
  "success": true,
  "message": "Reset instructions sent to email. Please check your inbox for the verification code.",
  "delivery": {
    "DeliveryMedium": "EMAIL",
    "Destination": "u***@igad.int",
    "AttributeName": "email"
  }
}

Error Responses

404 Not Found - User Not Found

Returned when email is not registered.
{
  "detail": "User not found"
}
Cognito Errors: UserNotFoundException or no users found in email lookup

400 Bad Request - Invalid Username Format

Returned when username/email format is invalid.
{
  "detail": "Invalid username format"
}
Cognito Error: InvalidParameterException

429 Too Many Requests - Rate Limit Exceeded

Returned when too many password reset requests.
{
  "detail": "Too many requests. Please try again later"
}
Cognito Error: LimitExceededException

500 Internal Server Error

Returned for unexpected errors.
{
  "detail": "Reset password error: {error_code}"
}
or
{
  "detail": "Failed to send reset code: {error_message}"
}

Implementation Details

Email to Username Lookup

Cognito uses UUID usernames internally, but users login with email. The API handles lookup:
# If input contains @, treat as email and lookup username
if "@" in username:
    users_response = cognito_client.list_users(
        UserPoolId=os.getenv("COGNITO_USER_POOL_ID"),
        Filter=f'email = "{username}"',
    )
    if users_response["Users"]:
        username = users_response["Users"][0]["Username"]  # Get UUID
    else:
        raise HTTPException(status_code=404, detail="User not found")
Code Reference: backend/app/tools/auth/routes.py:228

Cognito Forgot Password API

cognito_response = cognito_client.forgot_password(
    ClientId=os.getenv("COGNITO_CLIENT_ID"),
    Username=username  # Cognito UUID username
)
Code Reference: backend/app/tools/auth/routes.py:242

Email Delivery

Cognito sends password reset email using built-in email templates. No custom SES integration required. Verification Code:
  • 6 digits
  • Valid for 24 hours
  • Can be resent by calling endpoint again
Code Reference: backend/app/tools/auth/routes.py:246
Email templates can be customized in Cognito User Pool → Message customizations.

Step 2: Reset Password

POST /api/auth/reset-password

Completes password reset with verification code and new password.

Request

username
string
required
User’s email address (same as used in forgot-password request).Example: [email protected]
code
string
required
6-digit verification code received via email.Format: 6 digitsExample: "123456"
new_password
string
required
New password meeting Cognito requirements:
  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number
  • At least one special character
Example: "NewSecureP@ssw0rd"

Request Example

{
  "username": "[email protected]",
  "code": "123456",
  "new_password": "NewSecureP@ssw0rd"
}

Response

Success Response (200 OK)

success
boolean
required
Always true for successful password reset.
message
string
required
Confirmation message.Value: "Password reset successfully"

Success Response Example

{
  "success": true,
  "message": "Password reset successfully"
}

Error Responses

400 Bad Request - Invalid Verification Code

Returned when code doesn’t match.
{
  "detail": "Invalid verification code"
}
Cognito Error: CodeMismatchException

400 Bad Request - Expired Code

Returned when code has expired (>24 hours old).
{
  "detail": "Verification code has expired"
}
Cognito Error: ExpiredCodeException Solution: User must request new code via /api/auth/forgot-password

400 Bad Request - Invalid Password

Returned when password doesn’t meet requirements.
{
  "detail": "Password does not meet requirements"
}
Cognito Error: InvalidPasswordException Common Reasons:
  • Too short (less than 8 characters)
  • Missing uppercase/lowercase/number/special character
  • Contains username or email
  • Matches previous password

404 Not Found - User Not Found

Returned when username doesn’t exist.
{
  "detail": "User not found"
}
Cognito Error: UserNotFoundException

500 Internal Server Error

Returned for unexpected errors.
{
  "detail": "Reset password error: {error_code}"
}
or
{
  "detail": "Failed to reset password: {error_message}"
}

Implementation Details

Cognito Confirm Forgot Password API

cognito_client.confirm_forgot_password(
    ClientId=os.getenv("COGNITO_CLIENT_ID"),
    Username=request.username,
    ConfirmationCode=request.code,
    Password=request.new_password,
)
Code Reference: backend/app/tools/auth/routes.py:287

Password Validation

Cognito enforces password policy configured in User Pool settings:
  • Minimum length
  • Character requirements
  • Password history (prevent reuse)
  • Temporary password expiration
Configure password policy in Cognito User Pool → Policies → Password policy.

Complete Password Reset Flow

cURL Example

# Step 1: Request password reset
curl -X POST https://api.igad.int/api/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "username": "[email protected]"
  }'

# Response:
# {
#   "success": true,
#   "message": "Reset instructions sent to email...",
#   "delivery": {"DeliveryMedium": "EMAIL", "Destination": "u***@igad.int"}
# }

# Step 2: Check email for code (e.g., 123456)

# Step 3: Reset password with code
curl -X POST https://api.igad.int/api/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "username": "[email protected]",
    "code": "123456",
    "new_password": "NewSecureP@ssw0rd"
  }'

# Response:
# {"success": true, "message": "Password reset successfully"}

# Step 4: Login with new password
curl -X POST https://api.igad.int/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "[email protected]",
    "password": "NewSecureP@ssw0rd"
  }'

JavaScript (Fetch) Example

// Step 1: Initiate password reset
async function forgotPassword(email) {
  const response = await fetch('https://api.igad.int/api/auth/forgot-password', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      username: email,
    }),
  })

  const data = await response.json()

  if (response.ok) {
    console.log('Reset code sent to:', data.delivery.Destination)
    return true
  } else {
    console.error('Password reset failed:', data.detail)
    return false
  }
}

// Step 2: Complete password reset
async function resetPassword(email, code, newPassword) {
  const response = await fetch('https://api.igad.int/api/auth/reset-password', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      username: email,
      code: code,
      new_password: newPassword,
    }),
  })

  const data = await response.json()

  if (response.ok) {
    console.log('Password reset successful')
    // Redirect to login page
    window.location.href = '/login'
    return true
  } else {
    console.error('Password reset failed:', data.detail)
    
    // Handle specific errors
    if (data.detail.includes('expired')) {
      alert('Code expired. Please request a new one.')
    } else if (data.detail.includes('Invalid verification code')) {
      alert('Invalid code. Please check your email and try again.')
    } else if (data.detail.includes('Password does not meet requirements')) {
      alert('Password must be at least 8 characters with uppercase, lowercase, number, and special character.')
    }
    
    return false
  }
}

// Complete flow
async function handlePasswordReset() {
  const email = document.getElementById('email').value
  
  // Step 1: Send reset code
  const codeSent = await forgotPassword(email)
  
  if (codeSent) {
    // Show verification code input
    document.getElementById('code-section').style.display = 'block'
    
    // When user submits code and new password
    document.getElementById('reset-form').onsubmit = async (e) => {
      e.preventDefault()
      const code = document.getElementById('code').value
      const newPassword = document.getElementById('new-password').value
      
      await resetPassword(email, code, newPassword)
    }
  }
}

Python (Requests) Example

import requests
import time

def forgot_password(email: str) -> bool:
    """Step 1: Request password reset code."""
    response = requests.post(
        'https://api.igad.int/api/auth/forgot-password',
        json={'username': email}
    )
    
    if response.status_code == 200:
        data = response.json()
        print(f"Reset code sent to: {data['delivery']['Destination']}")
        return True
    else:
        print(f"Password reset failed: {response.json()['detail']}")
        return False

def reset_password(email: str, code: str, new_password: str) -> bool:
    """Step 2: Complete password reset with code."""
    response = requests.post(
        'https://api.igad.int/api/auth/reset-password',
        json={
            'username': email,
            'code': code,
            'new_password': new_password
        }
    )
    
    if response.status_code == 200:
        print("Password reset successful")
        return True
    else:
        error = response.json()['detail']
        print(f"Password reset failed: {error}")
        return False

# Complete flow
if __name__ == "__main__":
    email = input("Enter your email: ")
    
    # Step 1: Request reset code
    if forgot_password(email):
        print("\nCheck your email for the verification code.")
        
        # Step 2: Get code and new password from user
        code = input("Enter verification code: ")
        new_password = input("Enter new password: ")
        
        # Step 3: Reset password
        if reset_password(email, code, new_password):
            print("You can now log in with your new password.")

Security Considerations

Security Best Practices:
  1. Always use HTTPS in production
  2. Rate limit password reset requests (prevent abuse)
  3. Never expose whether email exists (return same response)
  4. Log password reset events for audit trail
  5. Invalidate all sessions after password reset

Code Expiration

Verification codes expire after 24 hours. Users must request new code if expired.

Rate Limiting

Cognito enforces rate limits:
  • Maximum 5 password reset requests per email per hour
  • Maximum 3 code verification attempts before lockout
Handle LimitExceededException with exponential backoff.

User Enumeration Prevention

Best Practice: Return same response whether user exists or not to prevent email enumeration attacks. Current Implementation: Returns 404 for non-existent users (consider changing for production).

Password Requirements

Enforce strong passwords:
  • Minimum 8 characters (consider 12+ for higher security)
  • Mixed case, numbers, special characters
  • Check against common password lists
  • Prevent reuse of recent passwords

Session Invalidation

After password reset, all existing sessions should be invalidated. Users must log in again with new password. To Implement:
# After successful password reset
cognito_client.admin_user_global_sign_out(
    UserPoolId=os.getenv('COGNITO_USER_POOL_ID'),
    Username=username
)

Audit Logging

Log password reset events:
  • Timestamp of request
  • Email address (hashed)
  • IP address
  • User agent
  • Success/failure
  • Verification attempts
Code Reference: Consider adding to backend/app/tools/auth/routes.py:218

Troubleshooting

User Not Receiving Email

Possible Causes:
  1. Email in spam/junk folder
  2. Incorrect email address
  3. Cognito email service not configured
  4. SES in sandbox mode (if using SES)
Solutions:
  • Check spam folder
  • Verify email in Cognito User Pool
  • Configure Cognito email settings
  • Move SES out of sandbox or verify recipient email

Code Always Invalid

Possible Causes:
  1. Code expired (>24 hours)
  2. Wrong code (check email carefully)
  3. Email mismatch (using different email than forgot-password)
Solutions:
  • Request new code
  • Copy code exactly from email (no spaces)
  • Use same email for both endpoints

Password Rejected

Possible Causes:
  1. Too short
  2. Missing character requirements
  3. Contains personal info (email, username)
  4. Matches previous password
Solutions:
  • Use password generator
  • Check Cognito password policy
  • Try completely different password

Testing

Unit Tests

import pytest
from fastapi.testclient import TestClient

def test_forgot_password_success(client: TestClient):
    response = client.post(
        "/api/auth/forgot-password",
        json={"username": "[email protected]"}
    )
    assert response.status_code == 200
    assert response.json()["success"] == True

def test_forgot_password_user_not_found(client: TestClient):
    response = client.post(
        "/api/auth/forgot-password",
        json={"username": "[email protected]"}
    )
    assert response.status_code == 404

def test_reset_password_invalid_code(client: TestClient):
    response = client.post(
        "/api/auth/reset-password",
        json={
            "username": "[email protected]",
            "code": "000000",
            "new_password": "NewP@ssw0rd123"
        }
    )
    assert response.status_code == 400
    assert "Invalid verification code" in response.json()["detail"]

Build docs developers (and LLMs) love