Password Recovery (Forgot Password) with AWS Cognito in iOS
In the previous articles, we implemented a complete authentication system with registration, login, session persistence, and automatic token refresh. Now we’ll tackle a critical flow: what happens when a user forgets their password?
What are we building?
In this article, we’ll implement:
- Request recovery code — User enters email, Cognito sends code
- Verify code and change password — User enters code + new password
- ForgotPasswordView — Screen with complete 2-step flow
- SignInView integration — «Forgot your password?» link
- Error handling — Incorrect codes, expired, invalid emails
The Forgot Password flow
Complete flow: User forgot password → enters email → Cognito sends code → user enters code + new password → password updated → user logs in with new password
User forgot password
↓ 1. Screen: Enter email
↓ User enters: jose@example.com
↓ App calls: forgotPassword(email)
↓ Cognito sends code to email
↓ 2. Screen: Enter code + new password
↓ App calls: confirmForgotPassword(email, code, newPassword)
↓ Password updated
↓ User logs in with new password
Prerequisites
- Have completed previous articles (especially Article 3: Login)
- Have CognitoAuthService implemented
- Understand code confirmation flow (similar to Article 2)
- Registered user with confirmed email
Step 1: Add Recovery Methods in CognitoAuthService
First we add two new methods to the authentication service. Add in CognitoAuthService.swift (after the refreshTokens method):
Method 1: forgotPassword(email:)
// MARK: - Password Recovery (Forgot Password)
/// Requests a password recovery code
func forgotPassword(email: String) async throws -> String {
guard let client = cognitoClient else {
throw NSError(domain: "CognitoAuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Cognito client not initialized"])
}
let forgotInput = ForgotPasswordInput(
clientId: clientId,
username: email
)
do {
let response = try await client.forgotPassword(input: forgotInput)
print("✅ Recovery code sent")
let destination = response.codeDeliveryDetails?.destination ?? "your email"
print("Destination: \(destination)")
return "Recovery code sent to \(destination)"
} catch {
print("❌ Error requesting recovery code: \(error)")
let errorMessage = error.localizedDescription
if errorMessage.contains("UserNotFoundException") {
throw NSError(domain: "CognitoAuthService", code: 6001,
userInfo: [NSLocalizedDescriptionKey: "User not found"])
} else if errorMessage.contains("LimitExceededException") {
throw NSError(domain: "CognitoAuthService", code: 6002,
userInfo: [NSLocalizedDescriptionKey: "Too many attempts. Wait a few minutes and try again"])
} else {
throw NSError(domain: "CognitoAuthService", code: 6000,
userInfo: [NSLocalizedDescriptionKey: "Error requesting code: \(errorMessage)"])
}
}
}
Method 2: confirmForgotPassword(email:code:newPassword:)
// MARK: - Confirm Password Recovery
/// Confirms password change with received code
func confirmForgotPassword(email: String, code: String, newPassword: String) async throws -> String {
guard let client = cognitoClient else {
throw NSError(domain: "CognitoAuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Cognito client not initialized"])
}
let confirmInput = ConfirmForgotPasswordInput(
clientId: clientId,
confirmationCode: code,
password: newPassword,
username: email
)
do {
_ = try await client.confirmForgotPassword(input: confirmInput)
print("✅ Password updated successfully")
return "Password updated successfully. You can now sign in with your new password"
} catch {
print("❌ Error confirming recovery: \(error)")
let errorMessage = error.localizedDescription
if errorMessage.contains("CodeMismatchException") {
throw NSError(domain: "CognitoAuthService", code: 7001,
userInfo: [NSLocalizedDescriptionKey: "Incorrect code. Verify and try again"])
} else if errorMessage.contains("ExpiredCodeException") {
throw NSError(domain: "CognitoAuthService", code: 7002,
userInfo: [NSLocalizedDescriptionKey: "Code has expired. Request a new one"])
} else if errorMessage.contains("InvalidPasswordException") {
throw NSError(domain: "CognitoAuthService", code: 7003,
userInfo: [NSLocalizedDescriptionKey: "Password does not meet minimum requirements"])
} else {
throw NSError(domain: "CognitoAuthService", code: 7000,
userInfo: [NSLocalizedDescriptionKey: "Error confirming recovery: \(errorMessage)"])
}
}
}
Step 2: Create ForgotPasswordView
Create file ForgotPasswordView.swift. The view handles two states:
enum ForgotPasswordStep {
case enterEmail // Step 1: Request code
case resetPassword // Step 2: Enter code + new password
}
Step 1 — Request code: Email field, «Send Code» button, calls authService.forgotPassword(email), on success automatically advances to Step 2.
Step 2 — Change password: Verification code field, new password field, confirm password field, visual password match validation, «Change Password» button, calls authService.confirmForgotPassword(email, code, newPassword), on success returns to SignInView.
Validations:
// Step 1
guard !email.isEmpty else { ... }
// Step 2
guard !code.isEmpty else { ... }
guard !newPassword.isEmpty else { ... }
guard newPassword == confirmPassword else { ... }
guard newPassword.count >= 8 else { ... }
UI/UX features: different icons per step, loading states with ProgressView, status messages, visual indicator for matching passwords, option to go back to previous step.
Step 3: Integrate in SignInView
Add the «Forgot your password?» link in SignInView.swift.
Add navigation state:
@State private var navigateToForgotPassword: Bool = false
Add link in the UI (after password field):
// Link to Forgot Password
HStack {
Spacer()
Button(action: {
navigateToForgotPassword = true
}) {
Text("Forgot your password?")
.font(.footnote)
.foregroundColor(.blue)
}
}
Add navigationDestination:
.navigationDestination(isPresented: $navigateToForgotPassword) {
ForgotPasswordView()
}


Step 4: Test the Complete Flow
Test 1: Successful flow
Open the app → go to SignInView → press «Forgot your password?» → Step 1: enter jose@example.com → press «Send Code» → console shows «✅ Recovery code sent» → alert: «Recovery code sent to j***@example.com» → view advances to Step 2 → check email → open email from AWS Cognito → subject: «Your verification code» → Step 2: enter code + new password + confirm → press «Change Password» → console: «✅ Password updated successfully» → returns to SignInView → sign in with new password → success.
Test 2: Email doesn’t exist
Enter: doesnotexist@example.com → press «Send Code» → alert: «User not found» ✅ No code was sent.
Test 3: Incorrect code
Request code for valid email → Step 2: enter code 999999 (incorrect) → press «Change Password» → alert: «Incorrect code. Verify and try again» ✅ Password did NOT change.
Test 4: Expired code
Cognito codes expire after 15 minutes. Request code → wait more than 15 minutes → enter expired code → alert: «Code has expired. Request a new one» → press «Go back to request code» → returns to Step 1.
Test 5: Weak password
Enter password that doesn’t meet requirements: new password «abc» (too short, no numbers, no uppercase) → local alert: «Password must be at least 8 characters». If passes local but Cognito rejects: «Password does not meet minimum requirements». Cognito requirements (configurable): minimum 8 characters, at least 1 uppercase, 1 lowercase, 1 number, 1 special character.
Test 6: Passwords don’t match
New password: Password123! / Confirm: Password456! → red indicator: «Passwords do not match» → button disabled.
Test 7: Rate limiting
If you request the code multiple times in a short period: Alert: «Too many attempts. Wait a few minutes and try again»

Conclusion
In this article, we implemented the complete password recovery flow with AWS Cognito. Now users can recover their accounts without needing technical support.
What we achieved:
- forgotPassword() — Request recovery code
- confirmForgotPassword() — Change password with code
- ForgotPasswordView — Complete 2-step screen
- SignInView integration — «Forgot your password?» link
- Error handling — 6 test scenarios covered
- UX best practices — Clear flow, visual feedback
Complete flow: User forgot password → «Forgot your password?» → enters email → forgotPassword() → receives code by email → enters code + new password → confirmForgotPassword() → password updated → signs in with new password.
Final reflection
I invite you to follow me on LinkedIn where I share content related to software development, best practices, and AWS technologies. You can also find me on Medium and explore my projects on GitHub. If you have questions or suggestions, don’t hesitate to connect.
Disclaimer
This article is for educational purposes. In production, you should: implement CAPTCHA or similar to prevent bot abuse, rate limiting on the client side, additional password validation, monitoring of suspicious attempts, secure logging (without sensitive data), and comprehensive testing.