Manejo de Sesión Expirada en AWS Cognito con Flutter

Introducción

Este artículo puede considerarse como la continuación de «Cierre Automático de Sesión por Inactividad en Flutter con AWS Cognito», donde implementamos un sistema para detectar inactividad y cerrar sesión automáticamente después de un tiempo determinado. Hasta ahora, nuestra aplicación Flutter permite mantener la sesión activa, cerrar sesión manualmente y manejar inactividad, pero aún hay un escenario que no hemos abordado: ¿qué sucede si la sesión expira por tiempo de vida del token?

AWS Cognito maneja la autenticación a través de tokens de sesión con tiempos de expiración configurables. Cuando un token expira, el usuario pierde acceso a la aplicación y necesita volver a autenticarse. Si no manejamos este evento correctamente, la app podría intentar realizar acciones sin autorización, lo que resultaría en errores inesperados.

Para solucionar esto, implementaremos un sistema que detecte cuando una sesión ha expirado y maneje la reautenticación de manera automática. El proceso funcionará de la siguiente manera:

  • Verificación de sesión: Cada vez que la app se inicie, verificará si la sesión sigue activa.
  • Renovación automática de sesión: Si el token de acceso ha expirado, intentaremos renovarlo con el Refresh Token.
  • Forzar cierre de sesión: Si el Refresh Token también ha expirado, redirigiremos al usuario a la pantalla de inicio de sesión.
  • Manejo en tiempo real: Si la sesión expira mientras la app está en uso, se notificará al usuario y se cerrará la sesión automáticamente.

Como en los artículos anteriores, mantendremos la implementación clara, sencilla y accesible, sin introducir patrones de arquitectura complejos. Nos enfocaremos en la integración de AWS Cognito con Flutter, asegurándonos de que cualquier persona pueda seguir los pasos sin complicaciones.

Más adelante, podremos optimizar este sistema con notificaciones previas a la expiración de sesión, opciones para extender la sesión manualmente o personalización del tiempo de vida de los tokens en AWS Cognito. Pero por ahora, nos centraremos en lo esencial: detectar y manejar sesiones expiradas en Flutter con AWS Cognito. ¡Vamos a ello! 🚀

¿Cóno funciona la expiración de sesión en AWS Cognito?

Esto es algo que vale la pena aprender, no solamente pensando en desarrollo de aplicaciones, sirve para conocer mejor cognito y adentrarnos en su funcionamiento.
AWS Cognito maneja la autenticación con tres tipos de tokens:

  • Access Token → Se usa para autenticar al usuario y expira en 1 hora por defecto.
  • ID Token → Contiene información del usuario y también expira en 1 hora.
  • Refresh Token → Permite obtener nuevos Access Tokens sin que el usuario vuelva a iniciar sesión. Expira en 30 días por defecto.

El problema ocurre cuando Access Token e ID Token expiran, pero no se ha implementado un mecanismo para renovarlos automáticamente usando el Refresh Token.

¿Qué es el Access Token?

El Access Token es un token de autorización que le permite a un usuario autenticado acceder a recursos protegidos, como APIs o servicios dentro de AWS.

Características del Access Token:

  • Valida que el usuario tiene acceso a un recurso (ejemplo: API Gateway, Lambda, DynamoDB).
  • Contiene los permisos (scopes) asociados al usuario.
  • Tiene una duración corta (por defecto, 1 hora) para mejorar la seguridad.
  • No contiene información detallada del usuario, solo lo necesario para verificar su acceso.

Si el Access Token expira, el usuario debe obtener uno nuevo con el Refresh Token o volver a autenticarse.

¿Qué es el ID Token?

 El ID Token contiene información sobre el usuario autenticado y se utiliza principalmente en autenticación basada en identidad.

 Características del ID Token:

  • Incluye información detallada del usuario (nombre, email, atributos personalizados).
  • Se usa para identificar al usuario, no para autorizar accesos.
  • Tiene una duración similar al Access Token (por defecto, 1 hora).
  • Está basado en JWT y sigue el estándar de OpenID Connect (OIDC).

El ID Token no se usa para acceder a recursos, solo para identificar al usuario.

CaracterísticaAccess TokenID Token
PropósitoVerificar permisos y acceso a recursos protegidosProporcionar información del usuario
Uso principalSe envía en cada petición a APIs protegidasSe usa en autenticación basada en identidad
Incluye información del usuarioNo solo username y permisosSí(nombre, email, atributos)
FormatoJWTJWT
Duración por defecto1 hora1 hora
Se renueva con RefreshSiSi

Si quieres acceder a una API protegida (ejemplo: AWS API Gateway, Lambda, DynamoDB) → Usa el Access Token.

Si necesitas identificar al usuario en la app (mostrar su nombre, email, foto, etc.) → Usa el ID Token.

Importante: Nunca uses el ID Token para hacer llamadas a APIs protegidas. Para eso, debes usar el Access Token.

Resumiendo podemos decir entonces que AWS Cognito maneja la autenticación con Access Token e ID Token, cada uno con un propósito distinto:

  • El Access Token se usa para autorizar acceso a recursos protegidos.
  • El ID Token se usa para identificar al usuario y mostrar su información.
  • Ambos tienen una duración limitada y se pueden renovar con el Refresh Token.

Ahora que comprendemos la diferencia, podemos manejar mejor la expiración de sesiones en nuestra aplicación Flutter con AWS Cognito.

Flujo para manejar las sesiones expiradas

Cada vez que la app se inicia, se intenta recuperar una sesión válida desde Cognito.
Si el Access Token ha expirado, se usa el Refresh Token para obtener uno nuevo.
Si el Refresh Token también ha expirado, el usuario debe iniciar sesión nuevamente.
Si la sesión expira mientras la app está en uso, se muestra una alerta y se redirige a LoginScreen.

Interfaz

Dado que esta funcionalidad no requiere modificaciones visibles en la interfaz de usuario, no será necesario agregar nuevos elementos visuales a las pantallas existentes. Sin embargo, realizaremos ajustes y agregaremos algunas líneas de código en el archivo de HomeScreen. Estos cambios, aunque esenciales para la lógica de detección de inactividad, no afectarán la apariencia de la aplicación.

Implementación de manejo de sesiones expiradas.

Archivo authentication_service.dart

import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import '../screens/login_screen.dart';

class AuthService {
  /// Verifica si la sesión aún es válida o si necesita renovarse
  Future<bool> checkSession() async {
    try {
      var session = await Amplify.Auth.fetchAuthSession();
      
      if (session.isSignedIn) {
        print("Sesión activa.");
        return true;
      } else {
        print("Sesión expirada. Necesita reautenticación.");
        return false;
      }
    } catch (e) {
      print("Error verificando sesión: $e");
      return false;
    }
  }

  /// Redirige a `LoginScreen` si la sesión está expirada
  void handleSessionExpiration(BuildContext context) async {
    bool isSessionValid = await checkSession();

    if (!isSessionValid) {
      Navigator.pushAndRemoveUntil(
        context,
        MaterialPageRoute(builder: (_) => LoginScreen()),
        (route) => false, // Elimina todas las pantallas anteriores
      );
    }
  }
}

Nos podemos concentrar en solo dos métodos del archivo, como ya sabemos el código completo vienen desde los artículos anteriores, pero vamos a concentrarnos por ahora solo en estos métodos. Vamos a explicar.

checkSession() → Usa Amplify.Auth.fetchAuthSession() para verificar si la sesión sigue activa.
handleSessionExpiration() → Si la sesión está expirada, redirige automáticamente al usuario a LoginScreen.

Como podemos ver gracias a los comentarios del código esta bastante sencillo entender el código.

Ahora pensemos en que esto puede suceder mientras estamos trabajando o usando la app, podríamos decir que es un problema en tiempo real, ¿Como manejariamos la expiración de sesión en tiempo real?

Agregaríamos un tercer método:

Future<void> someProtectedAction(BuildContext context) async {
  try {
    var result = await Amplify.Auth.getCurrentUser();
    print("Acción permitida. Usuario: ${result.username}");
  } catch (e) {
    if (e is AuthException) {
      print("Sesión expirada. Redirigiendo a Login...");
      handleSessionExpiration(context);
    }
  }
}

Si un usuario intenta realizar una acción cuando su sesión ha expirado, será redirigido a LoginScreen.

Ahora veremos como detectar y manejar las sesiones expiradas en HomeScreen, el archivo es home_screen.dart.

@override
void initState() {
  super.initState();
  _authService.handleSessionExpiration(context);
}

Cada vez que HomeScreen se carga, verifica si la sesión sigue activa.
Si ha expirado, el usuario es enviado a LoginScreen inmediatamente.

Como podemos ver fue bastante sencillo y claro todo el código que agregamos y modificamos.

Resumen

Este es un repaso del flujo que estamos pensando seguir:

  • Al abrir la app, se verifica si la sesión sigue activa.
  • Si ha expirado, se redirige automáticamente a LoginScreen.
  • Si la sesión expira mientras la app está en uso, el usuario también es enviado a LoginScreen.
  • Si la sesión es válida, el usuario sigue navegando normalmente.

Posibles mejoras para futuras implementaciones

Como ya hemos mencionado, nuestro código en todos los artículos tratamos de mantenerlo lo más simple posible, para no agregar complejidad extra, aunque esto implique que no estamos aplicando las mejores practicas o patrones a seguir. En este caso mencionaremos las posibles mejoras que podríamos hacer:

  • Mostrar una alerta antes de cerrar sesión, permitiendo al usuario renovar la sesión manualmente.
  • Configurar AWS Cognito para extender la duración del Refresh Token (por defecto es 30 días).
  • Permitir reautenticación sin cerrar sesión completamente.

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 *