Recuperación de Contraseña con AWS Cognito en Flutter: Guía Práctica

Introducción

Este artículo puede considerarse como la continuación de «Autenticación en Flutter con AWS Amplify y AWS Cognito: Primeros pasos», donde implementamos un sistema básico de autenticación en nuestra aplicación Flutter. En ese primer artículo, nos enfocamos en las funcionalidades esenciales: registro de usuarios, inicio de sesión y cierre de sesión utilizando AWS Cognito. Sin embargo, un sistema de autenticación completo necesita contemplar un escenario muy común: ¿qué pasa si el usuario olvida su contraseña?

En esta ocasión, vamos a abordar cómo recuperar una contraseña en AWS Cognito desde nuestra aplicación Flutter. Aunque comúnmente se habla de «recuperar contraseña», en realidad lo que hacemos es solicitar un cambio de contraseña. El proceso consiste en los siguientes pasos:

  1. Solicitud de recuperación: El usuario ingresa su correo electrónico y AWS Cognito envía un código de verificación a su email.
  2. Validación del código: El usuario ingresa el código recibido en la app.
  3. Cambio de contraseña: Si el código es correcto, se permite establecer una nueva contraseña.

Como en toda esta serie de artículos, nuestro principal objetivo es que la explicación sea clara, sencilla y accesible, especialmente para quienes no tienen mucha experiencia en Flutter. No estamos aquí para aprender buenas prácticas de Flutter ni para complicar el proyecto con estructuras avanzadas. Nos apegamos a lo más básico y directo, incluso si esto significa que el código puede parecer un poco amontonado en un solo archivo.

Queremos que cualquier persona pueda seguir estos pasos sin problema y entender cómo integrar AWS Cognito sin necesidad de conocimientos avanzados de Flutter. Más adelante, cuando hayamos explorado varias funcionalidades, seguramente consolidaremos todo en un proyecto más estructurado y con buenas prácticas de Flutter, pero por ahora, vamos al grano y nos enfocamos en la recuperación de la contraseña y Cognito.

Flujo del Cambio de Contraseña en AWS Cognito

Una vez que el usuario ha iniciado sesión y se encuentra en la pantalla principal (home_screen.dart), le daremos la opción de recuperar su contraseña en caso de que la haya olvidado. Para esto, realizaremos los siguientes ajustes en la aplicación:

  1. Modificación del archivo authentication_service.dart
    • Crearemos dos nuevos métodos:
      • Solicitar recuperación de contraseña → Envía un código de verificación al correo registrado del usuario.
      • Confirmar nueva contraseña → Toma el código de verificación y la nueva contraseña, y actualiza la contraseña en AWS Cognito.
  2. Actualización del archivo home_screen.dart
    • Ya teníamos el botón de cerrar sesión, pero ahora agregaremos lo siguiente:
      • Dos campos de texto:
        • Uno para ingresar el código de verificación enviado por AWS Cognito.
        • Otro para ingresar la nueva contraseña.
      • Dos botones adicionales:
        • Botón «Recuperar Contraseña» → Enviará la solicitud para que AWS Cognito mande un código de verificación al correo del usuario.
        • Botón «Confirmar Nueva Contraseña» → Toma el código de verificación y la nueva contraseña ingresada, y actualiza la contraseña del usuario en AWS Cognito.
  3. Enviar el correo de login_creen.dart a home_screeen.dart.
    • Actualmente, cuando el usuario inicia sesión exitosamente, lo redirigimos a home_screen.dart, pero no estamos enviando su correo electrónico a la siguiente pantalla. Como necesitamos este dato para recuperar la contraseña, modificaremos login_screen.dart para que al navegar a home_screen.dart, también enviemos el correo del usuario y lo tengamos disponible en la siguiente pantalla.

La idea es que el usuario pueda restablecer su contraseña siguiendo este flujo:

  1. Presiona el botón «Recuperar Contraseña», y AWS Cognito envía un código de verificación al correo asociado a la cuenta.
  2. Ingresa el código recibido y la nueva contraseña en los campos correspondientes.
  3. Presiona el botón «Confirmar Nueva Contraseña», y si todo está correcto, la contraseña se actualiza en AWS Cognito.

Con esta implementación, lograremos que el usuario pueda restablecer su contraseña de manera segura dentro de la aplicación.

Interfaz

Recordemos que estamos tomando como base el proyecto que hicimos en el primer artículo que hice de AWS y Flutter. Comparto aqui las pantallas, aunque en realidad la que nos interesa es la ultima. Recordemos que nos estamos centrado en AWS y como hacerlo funcionar en nuestras aplicaciones, no en interfaz gráfica, así que seguramente podría hacer lucir de mejor forma estas interfaces.

La primera pantalla es la que ya conocíamos sin cambios en la interfaz, la segunda interfaz si contiene:

  • Botón de cerrar sesión
  • Campo de texto para Introducir código de verificación(El que llega al correo)
  • Campo de texto para introducir la nueva contraseña
  • La opción de Recuperar contraseña
  • La opción de Confirmar nueva contraseña

Implementación de la recuperación de contraseña en AWS Cognito y Flutter

Archivos que se mantienen igual del ejemplo anterior:

  • main.dart
  • amplifyconfiguration.dart

Ahora veremos los que cambian aunque sea un poco. Comencemos con login_screen.dart

Este es el código:

import 'package:flutter/material.dart';
import 'package:flutter_aws_auth/home_screen.dart';
import 'authentication_service.dart';

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  // ignore: library_private_types_in_public_api
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  final _usernameSignupController = TextEditingController();
  final _passwordSignupController = TextEditingController();
  final _confirmationCodeController = TextEditingController();
  final _authService = AuthenticationService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Iniciar Sesión")),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _usernameSignupController,
              decoration: const InputDecoration(labelText: 'Email'),
            ),
            TextField(
              controller: _passwordSignupController,
              decoration: const InputDecoration(labelText: 'Contraseña'),
            ),
            ElevatedButton(
              onPressed: () async {
                var result = await _authService.signUp(
                  _usernameSignupController.text.trim(),
                  _passwordSignupController.text.trim(),
                );
                if (result == "confirmSignUp") {
                  _showConfirmationDialog();
                }
              },
              child: const Text("Registrarse"),
            ),
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(labelText: 'Usuario'),
            ),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(labelText: 'Contraseña'),
              obscureText: true,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                var result = await _authService.signIn(
                  _usernameController.text.trim(),
                  _passwordController.text.trim(),
                );
                if (result == "signedIn") {
                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(
                        builder: (_) => HomeScreen(
                              email: _usernameController.text.trim(),
                            )),
                  );
                } else if (result == "confirmSignUp") {
                  _showConfirmationDialog();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text("Error: No se pudo iniciar sesión")),
                  );
                }
              },
              child: const Text("Iniciar Sesión"),
            ),
            ElevatedButton(
              onPressed: () {
                _authService.signOut();
              },
              child: const Text("Sign out"),
            )
          ],
        ),
      ),
    );
  }

  void _showConfirmationDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("Confirmar Cuenta"),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: _confirmationCodeController,
                decoration:
                    InputDecoration(labelText: "Código de Confirmación"),
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () async {
                await _authService.confirmSignUp(
                  _usernameSignupController.text.trim(),
                  _confirmationCodeController.text.trim(),
                );
                Navigator.pop(context);
              },
              child: Text("Confirmar"),
            ),
          ],
        );
      },
    );
  }
}

El único código que cambia es este:

if (result == "signedIn") {
                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(
                        builder: (_) => HomeScreen(
                              email: _usernameController.text.trim(),
                            )),
                  );
                } else if (result == "confirmSignUp") {
                  _showConfirmationDialog();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text("Error: No se pudo iniciar sesión")),
                  );
                }

Lo que sucede es que, anteriormente, cuando invocábamos HomeScreen, simplemente lo hacíamos sin enviar ningún dato adicional. Ahora, en cambio, necesitamos pasar el correo electrónico del usuario a la siguiente pantalla.

Este correo lo obtenemos directamente del campo de texto donde el usuario lo ingresa al iniciar sesión. Podríamos mejorar este proceso considerablemente y manejar distintos escenarios, pero, como he mencionado antes, nuestro enfoque principal en esta serie de artículos es la integración con AWS, por lo que estamos dejando de lado validaciones más estrictas.

Por ejemplo, en un entorno más robusto, podríamos:

  • Verificar que el correo tenga un formato válido (evitar errores de escritura).
  • Restringir el uso de caracteres no permitidos.
  • Definir requisitos para la contraseña, como longitud mínima, caracteres especiales, etc.

Sin embargo, nuestro objetivo aquí es mantenerlo simple y centrarnos en la funcionalidad de AWS Cognito. Por eso, lo importante en este caso es que ahora necesitamos el correo electrónico para pasarlo a HomeScreen, ya que lo usaremos para el proceso de recuperación de contraseña.

Archivo authentication_service.dart

import 'package:amplify_flutter/amplify_flutter.dart';

class AuthenticationService {
  //Future<void> signUp(String email, String password) async {
  Future<String> signUp(String email, String password) async {
    try {
      SignUpResult result = await Amplify.Auth.signUp(
        username: email,
        password: password,
        options: SignUpOptions(userAttributes: {
          // 'email': email,
          AuthUserAttributeKey.email: email,
        }),
      );
      if (result.nextStep.signUpStep == "confirmSignUp") {
        print("Usuario necesita confirmar su cuenta.");
        return "confirmSignUp"; // ✅ Devuelve este string si se requiere confirmación
      }
      print("Usuario registrado correctamente.");
      return "signedUp"; // ✅ Usuario registrado correctamente
    } catch (e) {
      print("Error en el registro: $e");
      return "error"; // ✅ Devuelve "error" en caso de excepción
    }

    //   print("Usuario registrado correctamente.");
    // } catch (e) {
    //    print("Error en el registro: $e");
    //  }
  }

  /// Confirmar el registro del usuario con el código de verificación enviado al email
  Future<void> confirmSignUp(String email, String confirmationCode) async {
    try {
      SignUpResult result = await Amplify.Auth.confirmSignUp(
        username: email,
        confirmationCode: confirmationCode,
      );
      print("Usuario confirmado correctamente.");
    } catch (e) {
      print("Error al confirmar usuario: $e");
    }
  }

  // Future<void> signIn(String username, String password) async {
  Future<String> signIn(String username, String password) async {
    print('Iniciando sesión');
    print(username + password);
    try {
      SignInResult result = await Amplify.Auth.signIn(
        username: username,
        password: password,
      );
      /*  if (result.isSignedIn) {
        print('Usuario autenticado correctamente');
      }
      print(result);
    } catch (e) {
      print('Error en el inicio de sesión: $e');
      // print(result);
    }
    //print(result);*/
      if (result.isSignedIn) {
        print("Inicio de sesión exitoso.");
        return "signedIn"; // ✅ Esto activa la redirección a HomeScreen
      } else if (result.nextStep.signInStep == "confirmSignUp") {
        print("Usuario necesita confirmar su cuenta.");
        return "confirmSignUp";
      } else {
        print("Inicio de sesión incompleto.");
        return "incomplete";
      }
    } catch (e) {
      print("Error en el inicio de sesión: $e");
      return "error";
    }
  }

  /// Enviar código de recuperación al correo del usuario
  Future<void> resetPassword(String email) async {
    try {
      ResetPasswordResult result =
          await Amplify.Auth.resetPassword(username: email);
      print("Código enviado a $email");
    } catch (e) {
      print("Error al enviar código: $e");
    }
  }

  Future<void> confirmResetPassword(
      String email, String code, String newPassword) async {
    try {
      await Amplify.Auth.confirmResetPassword(
        username: email,
        newPassword: newPassword,
        confirmationCode: code,
      );
      print("Contraseña restablecida correctamente.");
    } catch (e) {
      print("Error al restablecer contraseña: $e");
    }
  }

  Future<void> signOut() async {
    try {
      await Amplify.Auth.signOut();
      print('Sesión cerrada');
    } catch (e) {
      print('Error al cerrar sesión: $e');
    }
  }
}

En realidad agregamos 2 métodos, estos son

  • resetPassword(String email)
  • confirmResetPassword(String email, String code, String newPassword)

resetPassword(String email) – Enviar Código de Recuperación.

Qué hace este método?

  • Envía una solicitud a AWS Cognito para iniciar el proceso de recuperación de contraseña.
  • AWS Cognito enviará un código de verificación al correo electrónico del usuario.
  • Si el proceso es exitoso, imprime "Código enviado a $email" en la consola.
  • Si ocurre un error (usuario no encontrado, problemas de conexión, etc.), lo captura y lo muestra en la consola.

Flujo de este método:

  1. Necesitamos tener el correo, nosotros lo traemos de la ventana anterior(Pero si quieres podrías pedirlo de nueva cuenta para asegurarte que lo introduzcan «Esto es lo que normalmente se hace»).
  2. Presiona el botón «Recuperar Contraseña».
  3. Cognito envía un correo con un código de verificación.

confirmResetPassword(String email, String code, String newPassword) – Confirmar Nueva Contraseña

¿Qué hace este método?

Recibe tres parámetros:

  • email: el correo del usuario.
  • code: el código de verificación que AWS Cognito envió por correo.
  • newPassword: la nueva contraseña que el usuario desea establecer.

Esto es lo que hace:

  • Llama a Amplify.Auth.confirmResetPassword() para validar el código y actualizar la contraseña en AWS Cognito.
  • Si la operación es exitosa, imprime "Contraseña restablecida correctamente." en la consola.
  • Si hay un error (código inválido, contraseña no cumple con las políticas, etc.), se captura y se muestra en la consola.

Flujo de este método:

  1. El usuario ingresa el código de verificación y la nueva contraseña.
  2. Presiona el botón «Confirmar Nueva Contraseña».
  3. Cognito valida el código y actualiza la contraseña del usuario.

Ahora vamos al archivo home_screen.dart:

import 'package:flutter/material.dart';
import 'authentication_service.dart';

class HomeScreen extends StatefulWidget {
  HomeScreen({super.key, required this.email});
  final String email;
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _authService = AuthenticationService();
  final _codeController = TextEditingController();
  final _newPasswordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Inicio")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                _authService.signOut();
              },
              child: const Text("Cerrar Sesión"),
            ),
            const SizedBox(height: 20),
            TextField(
              controller: _codeController,
              decoration: InputDecoration(labelText: "Código de verificación"),
            ),
            TextField(
              controller: _newPasswordController,
              decoration: InputDecoration(labelText: "Nueva Contraseña"),
              obscureText: true,
            ),
            SizedBox(height: 20),
            TextButton(
              onPressed: () async {
                print(widget.email);
                await _authService.resetPassword(widget.email);

              },
              child: const Text("Recuperar contraseña"),
            ),
            SizedBox(height: 20),
            TextButton(
              onPressed: () async {
                await _authService.confirmResetPassword(
                  widget.email,
                  _codeController.text.trim(),
                  _newPasswordController.text.trim(),
                );
              },
              child: const Text("Confirmar nueva contraseña"),
            ),
          ],
        ),
      ),
    );
  }
}

Primero tenemos la Definición de HomeScreen y Captura del Correo del Usuario:

Ahora HomeScreen recibe un parámetro adicional: el email del usuario. El correo electrónico es necesario para el proceso de recuperación de contraseña, ya que AWS Cognito usa este dato para enviar el código de verificación. Convertimos HomeScreen en un StatefulWidget para manejar la entrada del código de verificación y la nueva contraseña.

Definimos los controladores

  • _authService → Instancia del servicio de autenticación, que maneja las solicitudes a AWS Cognito.
  • _codeController → Captura el código de verificación que Cognito envía al correo del usuario.
  • _newPasswordController → Captura la nueva contraseña que el usuario quiere establecer.

Tenemos la Estructura básica → Contiene un AppBar con el título "Inicio" y un Column que organiza los widgets en vertical. Tenemos un ElevatedButton que permite al usuario cerrar sesión con AWS Cognito

Tenemos dos Campos de Texto para el Código y la Nueva Contraseña

  • Campo para ingresar el código de verificación enviado por AWS Cognito.
  • Campo para la nueva contraseña (se oculta con obscureText: true).

Llegamos al primer botón nuevo, que es el que tiene el código para recuperar solicitar el código de recuperación de contraseña.

Cuando el usuario presiona este botón, await _authService.resetPassword(widget.email):

  1. Se usa el correo del usuario (widget.email).
  2. Se llama a _authService.resetPassword(email), lo que envía un código de verificación al correo del usuario a través de AWS Cognito.

El usuario deberá revisar su correo y copiar el código para ingresarlo en el siguiente paso.

Cuando el usuario presiona este botón para Confirmar Nueva Contraseña, await _authService.confirmResetPassword(:

  1. Se toman los valores ingresados en los campos de código y nueva contraseña.
  2. Se llama a _authService.confirmResetPassword(), que envía la solicitud a AWS Cognito.
  3. Si la solicitud es exitosa, AWS Cognito actualiza la contraseña y el usuario ya puede iniciar sesión con su nueva contraseña.

Resumen General

SecciónFunción
Correo en HomeScreenAhora recibimos el email del usuario para usarlo en la recuperación de contraseña.
TextFieldsCapturan el código de verificación y la nueva contraseña.
Botón «Recuperar contraseña»Envía una solicitud para que AWS Cognito envíe un código de verificación al correo.
Botón «Confirmar nueva contraseña»Toma el código de verificación y la nueva contraseña para actualizarla en AWS Cognito.

Con estos cambios, ahora nuestra app Flutter con AWS Cognito soporta la recuperación y cambio de contraseña. El usuario puede solicitar un código de verificación, ingresarlo y establecer una nueva contraseña, todo desde la app.

Nos llegara un correo como este cuando se solicite el código:

Este es el código debemos de introducir junto a la nueva contraseña y listo, darle click en confirmar nueva contraseña para llamar al método

_authService.confirmResetPassword()

Con esto concluimos este artículo, en el que hemos logrado agregar a nuestro proyecto la funcionalidad de recuperación y confirmación de nueva contraseña con AWS Cognito en Flutter. Ahora, nuestra aplicación permite a los usuarios restablecer su contraseña de manera segura en caso de olvido, completando así un flujo de autenticación más robusto.

Este es solo un paso más en la integración de AWS en aplicaciones móviles, y aún tenemos mucho por explorar. Próximamente, seguiremos ampliando este proyecto con nuevas funcionalidades, optimizaciones y mejores prácticas.

Bueno, mi nombre es José Luján, desarrollador con 20 años de experiencia en el mundo del desarrollo, mobile, IA y crypto. Si tienes sugerencias sobre temas que te gustaría ver en los próximos artículos, serán bienvenidas.

Nos vemos pronto en la siguiente entrega, y si quieres seguir en contacto, me puedes encontrar en cualquier red social como @josedlujan. 🚀 ¡Hasta la próxima!

Deja una respuesta

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