Recuperación de contraseña (Forgot Password) con AWS Cognito en iOS

En los artículos anteriores implementamos un sistema completo de autenticación con registro, login, persistencia de sesión y refresh automático de tokens. Sin embargo, ¿qué pasa cuando un usuario olvida su contraseña? En este artículo implementaremos el flujo completo de recuperación de contraseña usando AWS Cognito.

¿Qué vamos a construir?

En este artículo implementaremos:

  1. Solicitar código de recuperación – Usuario ingresa email, Cognito envía código
  2. Verificar código y cambiar contraseña – Usuario ingresa código + nueva contraseña
  3. ForgotPasswordView – Pantalla con flujo completo de 2 pasos
  4. Integración en SignInView – Enlace “¿Olvidaste tu contraseña?”
  5. Manejo de errores – Códigos incorrectos, expirados, emails inválidos

El flujo de Forgot Password

Flujo completo:

Usuario olvidó contraseña
    ↓
1. Pantalla: Ingresar email
    ↓
Usuario ingresa: jose@example.com
    ↓
App llama: forgotPassword(email)
    ↓
Cognito envía código al email
    ↓
2. Pantalla: Ingresar código + nueva contraseña
    ↓
Usuario ingresa: 123456 + NuevaPassword123!
    ↓
App llama: confirmForgotPassword(email, code, newPassword)
    ↓
Cognito valida código y actualiza contraseña
    ↓
✅ Contraseña actualizada
    ↓
Usuario puede hacer login con nueva contraseña

Requisitos previos

  • Haber completado los artículos anteriores (especialmente Artículo 3: Login)
  • Tener CognitoAuthService implementado
  • Entender el flujo de confirmación de código (similar al Artículo 2)
  • Usuario registrado con email confirmado

Paso 1: Agregar métodos de recuperación en CognitoAuthService

Primero agregamos dos métodos nuevos al servicio de autenticación.

Agregar en CognitoAuthService.swift (después del método refreshTokens):

// Recuperación de Contraseña (Forgot Password)

/// Solicita un código de recuperación de contraseña
func forgotPassword(email: String) async throws -> String {
    guard let client = cognitoClient else {
        throw NSError(domain: "CognitoAuthService", code: -1,
                     userInfo: [NSLocalizedDescriptionKey: "Cliente de Cognito no inicializado"])
    }

    // Crear la solicitud de forgot password
    let forgotPasswordInput = ForgotPasswordInput(
        clientId: clientId,
        username: email
    )

    do {
        let response = try await client.forgotPassword(input: forgotPasswordInput)

        print("✅ Código de recuperación enviado")
        print("Destino: \(response.codeDeliveryDetails?.destination ?? "N/A")")

        let destination = response.codeDeliveryDetails?.destination ?? "tu email"
        return "Código de recuperación enviado a \(destination). Revisa tu email."

    } catch {
        print("❌ Error al solicitar código: \(error)")

        let errorMessage = error.localizedDescription
        if errorMessage.contains("UserNotFoundException") {
            throw NSError(domain: "CognitoAuthService", code: 6001,
                        userInfo: [NSLocalizedDescriptionKey: "Usuario no encontrado"])
        } else if errorMessage.contains("LimitExceededException") {
            throw NSError(domain: "CognitoAuthService", code: 6002,
                        userInfo: [NSLocalizedDescriptionKey: "Demasiados intentos. Espera unos minutos e intenta de nuevo"])
        } else {
            throw NSError(domain: "CognitoAuthService", code: 6000,
                        userInfo: [NSLocalizedDescriptionKey: "Error al solicitar código: \(errorMessage)"])
        }
    }
}

/// Confirma el cambio de contraseña con el código recibido
func confirmForgotPassword(email: String, code: String, newPassword: String) async throws -> String {
    guard let client = cognitoClient else {
        throw NSError(domain: "CognitoAuthService", code: -1,
                     userInfo: [NSLocalizedDescriptionKey: "Cliente de Cognito no inicializado"])
    }

    // Crear la solicitud de confirmación
    let confirmInput = ConfirmForgotPasswordInput(
        clientId: clientId,
        confirmationCode: code,
        password: newPassword,
        username: email
    )

    do {
        _ = try await client.confirmForgotPassword(input: confirmInput)

        print("✅ Contraseña actualizada exitosamente")

        return "Contraseña actualizada exitosamente. Ahora puedes iniciar sesión con tu nueva contraseña."

    } catch {
        print("❌ Error al cambiar contraseña: \(error)")

        let errorMessage = error.localizedDescription
        if errorMessage.contains("CodeMismatchException") {
            throw NSError(domain: "CognitoAuthService", code: 7001,
                        userInfo: [NSLocalizedDescriptionKey: "Código incorrecto. Verifica e intenta de nuevo"])
        } else if errorMessage.contains("ExpiredCodeException") {
            throw NSError(domain: "CognitoAuthService", code: 7002,
                        userInfo: [NSLocalizedDescriptionKey: "El código ha expirado. Solicita uno nuevo"])
        } else if errorMessage.contains("InvalidPasswordException") {
            throw NSError(domain: "CognitoAuthService", code: 7003,
                        userInfo: [NSLocalizedDescriptionKey: "La contraseña no cumple con los requisitos mínimos"])
        } else {
            throw NSError(domain: "CognitoAuthService", code: 7000,
                        userInfo: [NSLocalizedDescriptionKey: "Error al cambiar contraseña: \(errorMessage)"])
        }
    }
}

¿Qué hacen estos métodos?

1. forgotPassword(email:)

Solicita un código de recuperación a Cognito.

Proceso:

ForgotPasswordInput(clientId, username: email)
    ↓
client.forgotPassword(input)
    ↓
Cognito envía código al email del usuario
    ↓
response.codeDeliveryDetails.destination  // «j***@example.com»

Errores manejados: – UserNotFoundException (6001) – El email no existe en Cognito – LimitExceededException (6002) – Demasiados intentos (rate limiting) – Otros (6000) – Error genérico

2. confirmForgotPassword(email:code:newPassword:)

Confirma el cambio de contraseña con el código recibido.

Proceso:

ConfirmForgotPasswordInput(clientId, code, password, username)
    ↓
client.confirmForgotPassword(input)
    ↓
Cognito valida el código
    ↓
Si código es válido → actualiza contraseña

Errores manejados: – CodeMismatchException (7001) – Código incorrecto – ExpiredCodeException (7002) – Código expirado (15 min de validez) – InvalidPasswordException (7003) – Contraseña no cumple requisitos – Otros (7000) – Error genérico

Paso 2: Crear ForgotPasswordView

Ahora creamos la vista con el flujo completo de 2 pasos.

Crear archivo ForgotPasswordView.swift:

La vista maneja dos estados (enum ForgotPasswordStep): – .enterEmail – Paso 1: Solicitar código – .resetPassword – Paso 2: Cambiar contraseña

(Ver código completo en el repositorio)

Características principales:

  • Flujo de 2 pasos:
    • enum ForgotPasswordStep {
          case enterEmail      // Step 1
          case resetPassword   // Step 2
      }
  • Step 1: Solicitar código
    • Campo de email
    • Botón “Enviar Código”
    • Llama a authService.forgotPassword(email)
    • Al éxito → avanza automáticamente al Step 2
  • Step 2: Cambiar contraseña
    • Campo de código de verificación
    • Campo de nueva contraseña
    • Campo de confirmar contraseña
    • Validación visual de coincidencia de contraseñas
    • Botón “Cambiar Contraseña”
    • Llama a authService.confirmForgotPassword(email, code, newPassword)
    • Al éxito → vuelve a SignInView
  • Validaciones:
    • // 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:
    • Íconos diferentes por paso
    • Loading states con ProgressView
    • Mensajes de estado
    • Indicador visual de passwords coincidentes
    • Opción de volver al paso anterior

Paso 3: Integrar en SignInView

Agregamos el enlace “¿Olvidaste tu contraseña?” en SignInView.

Actualizar SignInView.swift:

  1. Agregar estado de navegación:
  2. @State private var navigateToForgotPassword: Bool = false
  3. Agregar enlace en la UI (después del campo de contraseña):
  4. // Enlace a Forgot Password
    HStack {
        Spacer()
        Button(action: {
            navigateToForgotPassword = true
        }) {
            Text(«¿Olvidaste tu contraseña?»)
                .font(.caption)
                .foregroundColor(.blue)
        }
    }
  5. Agregar navigationDestination:
  6. .navigationDestination(isPresented: $navigateToForgotPassword) {
        ForgotPasswordView()
    }

Paso 4: Probar el flujo completo

Ahora vamos a probar todo el flujo de recuperación de contraseña:

Prueba 1: Flujo completo exitoso

  • 1 Abrir la app
    • Ir a SignInView
    • Presionar “¿Olvidaste tu contraseña?”
  • 2 Step 1: Solicitar código
    • Ingresar: jose@example.com
    • Presionar “Enviar Código”
    • Console:
      • ✅ Código de recuperación enviado
        Destino: j***@example.com
      • Alert: “Código de recuperación enviado a j***@example.com”
      • La vista avanza automáticamente al Step 2
  • 3 Revisar email
    • Abrir el email asociado a la cuenta
    • Buscar email de AWS Cognito
    • Subject: “Your verification code”
    • Body: “Your verification code is 123456”
  • 4 Step 2: Cambiar contraseña
    • Ingresar código: 123456
    • Ingresar nueva contraseña: NuevaPassword123!
    • Confirmar contraseña: NuevaPassword123!
    • Indicador verde: “Las contraseñas coinciden”
    • Presionar “Cambiar Contraseña”
    • Console:
      • ✅ Contraseña actualizada exitosamente
    • Alert: “Contraseña actualizada exitosamente…”
    • La vista vuelve a SignInView
  • 5 Hacer login con nueva contraseña
    • Email: jose@example.com
    • Password: NuevaPassword123!
    • Presionar “Iniciar Sesión”
    • ✅ Login exitoso → HomeView

Prueba 2: Email no existe

  1. Step 1: Email inválido
    1. Ingresar: noexiste@example.com
    1. Presionar “Enviar Código”
    1. Alert: “Usuario no encontrado”
    1. ✅ No se envió código

Prueba 3: Código incorrecto

  1. Step 1: Solicitar código para email válido
  2. Step 2: Ingresar código incorrecto
    1. Código: 999999 (incorrecto)
    1. Nueva contraseña: Password123!
    1. Presionar “Cambiar Contraseña”
    1. Alert: “Código incorrecto. Verifica e intenta de nuevo”
    1. ✅ Contraseña NO cambió

Prueba 4: Código expirado

Los códigos de Cognito expiran después de 15 minutos.

  1. Step 1: Solicitar código
  2. Esperar más de 15 minutos
  3. Step 2: Ingresar código (ahora expirado)
    1. Alert: “El código ha expirado. Solicita uno nuevo”
    1. Presionar “Volver a solicitar código”
    1. ✅ Vuelve al Step 1 para solicitar código nuevo

Prueba 5: Contraseña débil

  1. Step 2: Ingresar contraseña que no cumple requisitos
    1. Código: 123456 (válido)
    1. Nueva contraseña: abc (muy corta, sin números, sin mayúsculas)
    1. Alert (local): “La contraseña debe tener al menos 8 caracteres”
    1. Si pasa validación local pero Cognito rechaza:
    1. Alert: “La contraseña no cumple con los requisitos mínimos”

Requisitos de contraseña en Cognito (configurables): – Mínimo 8 caracteres – Al menos 1 letra mayúscula – Al menos 1 letra minúscula – Al menos 1 número – Opcionalmente: símbolos especiales

Prueba 6: Contraseñas no coinciden

  1. Step 2:
    1. Nueva contraseña: Password123!
    1. Confirmar: Password456!
    1. Indicador rojo: “Las contraseñas no coinciden”
    1. Botón disabled o alert al presionar

Prueba 7: Rate limiting

Si solicitas código múltiples veces en poco tiempo:

  1. Intentos repetidos:
    1. Solicitar código
    1. Solicitar código otra vez
    1. Solicitar código otra vez
    1. Alert: “Demasiados intentos. Espera unos minutos e intenta de nuevo”

Conclusión

En este artículo implementamos el flujo completo de recuperación de contraseña con AWS Cognito. Ahora los usuarios pueden recuperar su cuenta si olvidan la contraseña.

Lo que logramos:

forgotPassword() – Solicitar código de recuperación

confirmForgotPassword() – Cambiar contraseña con código

ForgotPasswordView – UI de 2 pasos (email → código+password)

Integración en SignInView – Enlace “¿Olvidaste tu contraseña?”

Validaciones completas – Email, código, password strength, coincidencia

Manejo de errores – Usuarios no encontrados, códigos incorrectos/expirados, rate limiting

Flujo completo:

Usuario olvidó contraseña
    → SignInView → «¿Olvidaste tu contraseña?»
    → ForgotPasswordView Step 1
    → Ingresar email
    → forgotPassword(email)
    → Cognito envía código al email
    → ForgotPasswordView Step 2
    → Ingresar código + nueva contraseña
    → confirmForgotPassword(email, code, newPassword)
    → Cognito valida y actualiza contraseña
    → Volver a SignInView
    → Login con nueva contraseña
    → ✅ Acceso restaurado

¿Qué sigue?

En el próximo artículo implementaremos: – Artículo 9: Cambio de contraseña – Usuario logueado cambia su password – Artículo 10: Actualizar perfil – Cambiar email, nombre, atributos custom – Artículo 11: Eliminar cuenta – Usuario puede eliminar su propia cuenta

Reflexión final

El flujo de “Forgot Password” es CRÍTICO para la retención de usuarios. Sin esto: – Usuarios que olvidan su contraseña → pierden acceso permanente – Deben crear una cuenta nueva → pierden su historial/datos – Frustración → abandonan la app

Con Forgot Password: – Recuperación self-service en minutos – No requiere soporte técnico – Experiencia fluida y profesional

Mejores prácticas implementadas:

1. Flujo de 2 pasos – Claro y guiado

2. Validaciones frontend – Feedback inmediato al usuario

3. Mensajes claros – El usuario sabe qué hacer en cada paso

4. Manejo de errores – Cada error tiene su mensaje específico

5. Rate limiting – Protección contra spam/abuse

José Luján es un desarrollador móvil especializado en iOS, Android y desarrollo multiplataforma (Flutter). Experto en integración de servicios AWS en aplicaciones móviles y autor de múltiples artículos técnicos. También forma parte del programa Amazon Community Builders.

Disclaimer: Este artículo es con fines educativos. En producción, deberías: – Implementar CAPTCHA o similar para prevenir abuse del endpoint de forgot password – Considerar agregar verificación por SMS como alternativa al email – Implementar logging de intentos de reset para detectar patrones sospechosos – Agregar notificaciones al usuario cuando se cambia su contraseña (email de confirmación) – Considerar políticas de bloqueo temporal después de múltiples intentos fallidos – En algunos casos, requerir re-autenticación antes de permitir cambio de password (si el usuario está logueado)

Deja una respuesta

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