Overview
The password reset flow consists of two endpoints:
Forgot Password - Initiates password reset and sends verification code via email
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:
User requests password reset with email
Cognito generates 6-digit verification code
Code sent to user’s email (via Cognito email templates)
User submits code + new password
Cognito validates and updates password
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
User’s email address. The API automatically looks up the Cognito username (UUID) from email. Example: [email protected]
Request Example
Response
Success Response (200 OK)
Always true for successful requests.
Instructions for next steps. Value: "Reset instructions sent to email. Please check your inbox for the verification code."
Cognito delivery details for verification code. Always "EMAIL" for email delivery.
Masked email address showing where code was sent. Example: "u***@igad.int"
The attribute used for delivery (typically "email").
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
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
User’s email address (same as used in forgot-password request). Example: [email protected]
6-digit verification code received via email. Format: 6 digitsExample: "123456"
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)
Always true for successful password reset.
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 ( " \n Check 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:
Always use HTTPS in production
Rate limit password reset requests (prevent abuse)
Never expose whether email exists (return same response)
Log password reset events for audit trail
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:
Email in spam/junk folder
Incorrect email address
Cognito email service not configured
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:
Code expired (>24 hours)
Wrong code (check email carefully)
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:
Too short
Missing character requirements
Contains personal info (email, username)
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" ]