Change Password with AWS Cognito in iOS
In the previous article, we implemented the password recovery flow for users who forgot their password. But what happens when a logged-in user wants to change their password for security reasons? In this article, we’ll implement the Change Password flow.
What are we building?
- changePassword method — Change password requiring the current one
- ChangePasswordView — Screen to enter current password + new password
- HomeView integration — «Change Password» button in settings
- Validations — Verify current password, new password strength, matching
- Error handling — Incorrect password, weak password, etc.
Difference from Forgot Password
| Aspect | Forgot Password (Article 8) | Change Password (this article) |
|---|---|---|
| User logged in | ❌ NOT logged in | ✅ IS logged in |
| Remembers password | ❌ Does NOT remember | ✅ DOES remember |
| Requires code | ✅ Yes, via email | ❌ No |
| Validates current | ❌ No | ✅ Yes, with previousPassword |
| After completion | ⚠️ Must sign in again | ✅ Stays logged in |
The Change Password flow
User logged in at HomeView → presses «Change Password» → ChangePasswordView → enter: CURRENT password + New password + Confirm → App calls changePassword(accessToken, currentPassword, newPassword) → Cognito validates current password → updates to new password → user STAYS logged in.
Prerequisites
- Have completed previous articles (especially Article 3: Login)
- Have CognitoAuthService implemented
- Have HomeView with navigation
- User logged in with tokens in Keychain
Step 1: Add changePassword Method in CognitoAuthService
Add the method to change password in CognitoAuthService.swift:
// MARK: - Change Password
/// Changes the authenticated user's password
/// - Parameters:
/// - currentPassword: The user's current password (required for validation)
/// - newPassword: The new password to set
func changePassword(currentPassword: String, newPassword: String) async throws -> String {
// Get Access Token from Keychain
guard let accessToken = KeychainService.shared.getAccessToken() else {
throw NSError(domain: "CognitoAuthService", code: 8001,
userInfo: [NSLocalizedDescriptionKey: "No active session. Please sign in"])
}
guard let client = cognitoClient else {
throw NSError(domain: "CognitoAuthService", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Cognito client not initialized"])
}
let changeInput = ChangePasswordInput(
accessToken: accessToken,
previousPassword: currentPassword,
proposedPassword: newPassword
)
do {
_ = try await client.changePassword(input: changeInput)
print("✅ Password changed successfully")
return "Password updated successfully"
} catch {
print("❌ Error changing password: \(error)")
let errorMessage = error.localizedDescription
if errorMessage.contains("NotAuthorizedException") {
throw NSError(domain: "CognitoAuthService", code: 8002,
userInfo: [NSLocalizedDescriptionKey: "Current password is incorrect"])
} else if errorMessage.contains("InvalidPasswordException") {
throw NSError(domain: "CognitoAuthService", code: 8003,
userInfo: [NSLocalizedDescriptionKey: "Password does not meet minimum requirements"])
} else if errorMessage.contains("LimitExceededException") {
throw NSError(domain: "CognitoAuthService", code: 8004,
userInfo: [NSLocalizedDescriptionKey: "Too many attempts. Wait a few minutes and try again"])
} else {
throw NSError(domain: "CognitoAuthService", code: 8000,
userInfo: [NSLocalizedDescriptionKey: "Error changing password: \(errorMessage)"])
}
}
}
Key points:
- ✅ Requires Access Token — user MUST be logged in
- ✅ Validates current password with previousPassword
- ✅ Uses error codes 8000-8004
- ✅ User stays logged in after change

Step 2: Create ChangePasswordView
Create the file ChangePasswordView.swift:
import SwiftUI
struct ChangePasswordView: View {
@Environment(\.dismiss) private var dismiss
@State private var currentPassword: String = ""
@State private var newPassword: String = ""
@State private var confirmPassword: String = ""
@State private var isLoading: Bool = false
@State private var message: String = ""
@State private var showAlert: Bool = false
private let authService = CognitoAuthService()
// Password strength
private var passwordStrength: Int {
var score = 0
if newPassword.count >= 8 { score += 1 }
if newPassword.contains(where: { $0.isUppercase }) { score += 1 }
if newPassword.contains(where: { $0.isNumber }) { score += 1 }
if newPassword.contains(where: { "!@#$%^&*".contains($0) }) { score += 1 }
return score
}
private var strengthColor: Color {
switch passwordStrength {
case 0...1: return .red
case 2: return .orange
case 3: return .yellow
default: return .green
}
}
private var passwordsMatch: Bool {
!confirmPassword.isEmpty && newPassword == confirmPassword
}
private var isFormValid: Bool {
!currentPassword.isEmpty && newPassword.count >= 8 && passwordsMatch
}
var body: some View {
VStack(spacing: 20) {
Text("Change Password")
.font(.largeTitle)
.fontWeight(.bold)
.padding(.bottom, 20)
// Current password
SecureField("Current password", text: $currentPassword)
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// New password
SecureField("New password", text: $newPassword)
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// Strength indicator (4 bars with color)
HStack(spacing: 4) {
ForEach(0..<4, id: \.self) { index in
Rectangle()
.fill(index < passwordStrength ? strengthColor : Color(.systemGray5))
.frame(height: 4)
.cornerRadius(2)
}
}
// Confirm new password
SecureField("Confirm new password", text: $confirmPassword)
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
// Match indicator
if !confirmPassword.isEmpty {
Text(passwordsMatch ? "✅ Passwords match" : "❌ Passwords do not match")
.font(.caption)
.foregroundColor(passwordsMatch ? .green : .red)
}
// Change Password button
Button(action: { changePassword() }) {
Text(isLoading ? "" : "Change Password")
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.overlay { if isLoading { ProgressView().tint(.white) } }
}
.background(isFormValid && !isLoading ? Color.blue : Color.gray)
.cornerRadius(10)
.disabled(!isFormValid || isLoading)
Spacer()
}
.padding(.horizontal, 30)
.padding(.top, 40)
.alert("Change Password", isPresented: $showAlert) {
Button("OK") {
if message.contains("successfully") { dismiss() }
}
} message: {
Text(message)
}
}
private func changePassword() {
guard newPassword != currentPassword else {
message = "New password must be different from current password"
showAlert = true
return
}
isLoading = true
Task {
do {
let result = try await authService.changePassword(
currentPassword: currentPassword,
newPassword: newPassword
)
await MainActor.run {
isLoading = false
message = result
showAlert = true
}
} catch {
await MainActor.run {
isLoading = false
message = error.localizedDescription
showAlert = true
}
}
}
}
}

View features:
- ✅ 3 fields (current, new, confirm)
- ✅ Visual password strength indicator (4 bars with color: red/orange/yellow/green)
- ✅ Real-time requirements checklist
- ✅ Match indicator
- ✅ Button disabled until form is valid
Step 3: Integrate in HomeView
Modify HomeView.swift to add navigation to ChangePasswordView:
struct HomeView: View {
@Binding var isLoggedIn: Bool
@State private var showingSignOut = false
@State private var navigateToChangePassword = false // New
var body: some View {
VStack(spacing: 20) {
Text("Welcome!")
.font(.largeTitle)
.fontWeight(.bold)
// Change Password button (New)
Button(action: { navigateToChangePassword = true }) {
Label("Change Password", systemImage: "lock.rotation")
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
}
.background(Color.blue)
.cornerRadius(10)
.padding(.horizontal, 30)
// Sign Out button
Button(action: { showingSignOut = true }) {
Text("Sign Out")
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
}
.background(Color.red)
.cornerRadius(10)
.padding(.horizontal, 30)
}
.navigationDestination(isPresented: $navigateToChangePassword) {
ChangePasswordView()
}
// ... rest of modifiers
}
}
Integration highlights:
- ✅ "Change Password" button in HomeView
- ✅ Uses lock.rotation icon
- ✅ Blue color to differentiate from "Sign Out" (red)
- ✅ navigationDestination navigates to ChangePasswordView
Step 4: Test the Complete Flow
Test 1: Successful change
Sign in → HomeView → press "Change Password" → enter current password: Password123 → new password: NewPass456! → confirm: NewPass456! → strength indicator: green → passwords match: ✅ → press "Change Password" → console: "✅ Password changed successfully" → alert: "Password updated successfully" → press OK → returns to HomeView.
Test 2: Incorrect current password
Enter INCORRECT current password: WrongPass123 → new password: NewPass456 → press "Change Password" → alert: "Current password is incorrect" → stays in ChangePasswordView.
Test 3: Weak new password
Enter correct current password → weak new password: "abc" → button DISABLED (gray, strength indicator red) → user must improve password before proceeding.
Test 4: Passwords don't match
Current: Password123 → New: NewPass456 → Confirm: NewPass789 → red indicator: "Passwords do not match" → button DISABLED.
Test 5: Same password as current
Current: Password123 → New: Password123 → Confirm: Password123 → press "Change Password" → alert: "New password must be different from current password".
Test 6: Too many attempts
Try with incorrect current password 5-6 times → alert: "Too many attempts. Wait a few minutes and try again".
Test 7: No active session
If tokens deleted from Keychain while in ChangePasswordView → alert: "No active session. Please sign in".
Key differences: Change Password vs Forgot Password
| Aspect | Change Password | Forgot Password |
|---|---|---|
| User logged in | ✅ Yes | ❌ No |
| Remembers password | ✅ Yes | ❌ No |
| Requires code | ❌ No | ✅ Yes |
| Validates current | ✅ Yes | ❌ No |
| Signs out after | ❌ No | ⚠️ Must log in |
| Cognito API | changePassword() | forgotPassword() + confirmForgotPassword() |
| Flow | 1 step (form) | 2 steps (email + code) |
Best practices
Real-time validation: show password strength indicators while typing, requirements checklist with visual feedback.
Password UX: disable button if requirements not met, use clear colors (green=good, red=bad, yellow=fair).
Security: verify that new ≠ current, show clear requirements BEFORE submitting, handle "incorrect password" without exposing security details, inform about attempt limits.
Session persistence: do NOT sign out after changing password — tokens remain valid.
Common errors and solutions
Error 1: "No active session"
Cause: Tokens not in Keychain or expired. Solution: Ensure user has signed in. If tokens expired, use the refresh flow (Article 6).
Error 2: Button always disabled
Cause: isFormValid too strict. Solution: Verify new password ≥ 8 characters, new == confirm, all fields filled.
Error 3: Alert doesn't dismiss view
Cause: Condition doesn't detect success. Solution: Verify message contains "updated successfully".
Error 4: UI doesn't respond after change
Cause: Didn't use await MainActor.run. Solution: Always update UI from MainActor.
Summary
In this article, we implemented:
- ✅ changePassword method in CognitoAuthService — requires Access Token, validates current password, error codes 8000-8004
- ✅ ChangePasswordView — 3 fields, visual strength indicator (4 color bars), real-time requirements checklist, match indicator
- ✅ HomeView integration — "Change Password" button with lock.rotation icon, navigationDestination
- ✅ Exhaustive validations: current password correct, new sufficiently strong (≥8 chars), new ≠ current, new == confirm
- ✅ Error handling: incorrect password, weak password, attempt limits
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.