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:
- Solicitar código de recuperación – Usuario ingresa email, Cognito envía código
- Verificar código y cambiar contraseña – Usuario ingresa código + nueva contraseña
- ForgotPasswordView – Pantalla con flujo completo de 2 pasos
- Integración en SignInView – Enlace “¿Olvidaste tu contraseña?”
- 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
}
- enum ForgotPasswordStep {
- 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 { … }
- // Step 1
- 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:
- Agregar estado de navegación:
- @State private var navigateToForgotPassword: Bool = false
- Agregar enlace en la UI (después del campo de contraseña):
- // Enlace a Forgot Password
HStack {
Spacer()
Button(action: {
navigateToForgotPassword = true
}) {
Text(«¿Olvidaste tu contraseña?»)
.font(.caption)
.foregroundColor(.blue)
}
} - Agregar navigationDestination:
- .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
- ✅ Código de recuperación enviado
- 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
- Step 1: Email inválido
- Ingresar: noexiste@example.com
- Presionar “Enviar Código”
- Alert: “Usuario no encontrado”
- ✅ No se envió código
Prueba 3: Código incorrecto
- Step 1: Solicitar código para email válido
- Step 2: Ingresar código incorrecto
- Código: 999999 (incorrecto)
- Nueva contraseña: Password123!
- Presionar “Cambiar Contraseña”
- Alert: “Código incorrecto. Verifica e intenta de nuevo”
- ✅ Contraseña NO cambió
Prueba 4: Código expirado
Los códigos de Cognito expiran después de 15 minutos.
- Step 1: Solicitar código
- Esperar más de 15 minutos
- Step 2: Ingresar código (ahora expirado)
- Alert: “El código ha expirado. Solicita uno nuevo”
- Presionar “Volver a solicitar código”
- ✅ Vuelve al Step 1 para solicitar código nuevo
Prueba 5: Contraseña débil
- Step 2: Ingresar contraseña que no cumple requisitos
- Código: 123456 (válido)
- Nueva contraseña: abc (muy corta, sin números, sin mayúsculas)
- Alert (local): “La contraseña debe tener al menos 8 caracteres”
- Si pasa validación local pero Cognito rechaza:
- 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
- Step 2:
- Nueva contraseña: Password123!
- Confirmar: Password456!
- Indicador rojo: “Las contraseñas no coinciden”
- Botón disabled o alert al presionar
Prueba 7: Rate limiting
Si solicitas código múltiples veces en poco tiempo:
- Intentos repetidos:
- Solicitar código
- Solicitar código otra vez
- Solicitar código otra vez
- …
- 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)