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

AspectForgot 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
Summary of changePassword method — error codes 8000-8004

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
                }
            }
        }
    }
}
ChangePasswordView with strength indicator and color bars

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

AspectChange PasswordForgot 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 APIchangePassword()forgotPassword() + confirmForgotPassword()
Flow1 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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *