Cierre Automático de Sesión por Inactividad en Flutter con AWS Cognito
Introducción
Este artículo puede considerarse como la continuación de «Protección de Rutas y Autenticación Persistente en Flutter con AWS Cognito», donde implementamos un sistema de autenticación que mantiene la sesión activa y restringe el acceso a ciertas pantallas según el estado del usuario. Hasta ahora, nuestra aplicación Flutter permite que un usuario inicie sesión, navegue por las rutas protegidas y cierre sesión manualmente.
Sin embargo, ¿qué sucede si el usuario deja la aplicación abierta y se aleja del dispositivo sin cerrar sesión? En muchos sistemas, esto representa un problema de seguridad, ya que cualquier persona con acceso al dispositivo podría utilizar la aplicación sin restricciones. Para abordar esta situación, implementaremos un cierre automático de sesión por inactividad.
El proceso funcionará de la siguiente manera:
- Detección de inactividad: Un temporizador inicia automáticamente cuando el usuario accede a la aplicación.
- Reinicio del temporizador: Cualquier interacción con la pantalla reinicia el temporizador.
- Cierre de sesión automático: Si no hay actividad después de un tiempo determinado (por ejemplo, 5 minutos), la aplicación cerrará la sesión automáticamente y redirigirá al usuario a la pantalla de inicio de sesión.
Como en los artículos anteriores, nuestro objetivo es mantener la implementación clara, sencilla y accesible, sin introducir patrones de arquitectura avanzados. 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 mejorar este sistema agregando funcionalidades como notificaciones antes del cierre de sesión, personalización del tiempo de inactividad o la opción de extender la sesión manualmente. Pero por ahora, nos centraremos en lo esencial: detectar inactividad y cerrar la sesión del usuario automáticamente en Flutter con AWS Cognito. ¡Vamos a ello! 🚀
Flujo del Cierre Automático de Sesión por Inactividad
1. Inicio del temporizador al entrar a la aplicación
Cuando el usuario accede a HomeScreen
, la aplicación inicia un temporizador de inactividad. Este temporizador comienza a contar desde el momento en que la pantalla se carga y se ejecuta en segundo plano.
Flujo inicial:
- El usuario inicia sesión con AWS Cognito y entra a
HomeScreen
. - Se activa un temporizador que cuenta 5 minutos.
- Si el usuario interactúa, el temporizador se reinicia.
2. Detección de interacción del usuario
Para evitar que la sesión se cierre mientras el usuario está activo, detectamos interacciones en la pantalla. Cada vez que el usuario toca o desliza en la pantalla, el temporizador se reinicia.
Flujo de detección de actividad:
- Si el usuario toca la pantalla, el temporizador se cancela y se reinicia.
- Si el usuario desliza el dedo en la pantalla, también se reinicia el temporizador.
- Si no hay interacción durante 5 minutos, la sesión se cierra automáticamente.
3. Cierre automático de sesión si no hay interacción
Si el usuario no interactúa con la pantalla dentro del tiempo establecido, la sesión se cierra automáticamente y se redirige a la pantalla de inicio de sesión.
Flujo de cierre de sesión:
- Si pasan 5 minutos sin interacción,
_logoutUser()
se ejecuta. - Se llama a
Amplify.Auth.signOut()
para cerrar la sesión en AWS Cognito. - Se redirige a
LoginScreen
, eliminando todas las pantallas anteriores.
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 en la estructura del árbol de widgets y añadiremos 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 del cierre automático de sesión por inactividad.
Archivo home_screen.dart:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_aws_auth/details_screen.dart';
import 'package:flutter_aws_auth/help_screen.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();
//Timer
Timer? _inactivityTimer;
@override
void initState() {
super.initState();
_resetInactivityTimer();
}
void _resetInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = Timer(const Duration(seconds: 8), () {
_logoutUser();
});
}
void _logoutUser() async {
await _authService.signOut(context);
}
@override
void dispose() {
_inactivityTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _resetInactivityTimer,
onPanDown: (_) => _resetInactivityTimer(),
child: Scaffold(
appBar: AppBar(title: const Text("Inicio")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_authService.signOut(context);
},
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"),
),
Row(
children: [
ElevatedButton(
onPressed: () async {
bool isLoggedIn = await _authService.checkUserSession();
if (isLoggedIn) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
const HelpScreen()), // Usuario autenticado -> Ruta 2
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
const DetailScreen()), // Usuario no autenticado -> Ruta 1
);
}
},
child: const Text("Ruta 1"),
),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const HelpScreen(
)),
);
},
child: const Text("Ruta 2"),
),
],
)
],
),
),
),
);
}
}
Este código define la pantalla HomeScreen
, que es la pantalla principal de la aplicación después de que el usuario haya iniciado sesión. Además de manejar la navegación y autenticación, implementa un temporizador de inactividad que cierra automáticamente la sesión si el usuario no interactúa con la app durante un período determinado.
Nos vamos a concentrar en la parte que hemos modificado, recordemos que este código viene de los artículos anteriores que he publicado.
_inactivityTimer
→ Temporizador que se activará para detectar inactividad del usuario.
Hemos agregado la variables que sirve como temporizador, ahora vamos agregar 3 métodos:
_resetInactivityTimer()
→ Se llama cada vez que el usuario toca o desliza en la pantalla._logoutUser()
→ Se ejecuta cuando el temporizador llega a 8 segundos, cerrando la sesión.dispose()
→ Cancela el temporizador cuando la pantalla se destruye.
Cuando HomeScreen
se carga, se inicia el temporizador de inactividad con _resetInactivityTimer()
.
Cada vez que el usuario interactúe con la pantalla, este temporizador se reinicia. Si no hay actividad en 8 segundos, se ejecuta _logoutUser()
para cerrar la sesión.
_logoutUser()
llama a signOut()
en auth_service.dart
, cerrando la sesión del usuario en AWS Cognito y redirigiéndolo a LoginScreen
.
Si el usuario cierra la app o cambia de pantalla, el temporizador se cancela para evitar errores o ejecución innecesaria en segundo plano.
Podemos ver que dentro del arbol de Widget, hemos colocado el GestureDetector. GestureDetector
permite detectar interacciones del usuario.
Cada vez que el usuario toca o desliza en la pantalla, _resetInactivityTimer()
se llama para evitar el cierre de sesión.
En resumen:
HomeScreen
es la pantalla principal tras iniciar sesión.- Se implementa un sistema de cierre de sesión automático por inactividad con
Timer
. - Cada interacción del usuario reinicia el temporizador para evitar el cierre de sesión.
- Al pasar 8 segundos sin actividad, la sesión se cierra y el usuario es redirigido a
LoginScreen
. - Los botones permiten recuperar y cambiar la contraseña del usuario.
- Se protege el acceso a ciertas rutas según el estado de autenticación del usuario.
Archivo authentication_service.dart:
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_aws_auth/login_screen.dart';
class AuthenticationService {
Future<String> signUp(String email, String password) async {
try {
SignUpResult result = await Amplify.Auth.signUp(
username: email,
password: password,
options: SignUpOptions(userAttributes: {
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
}
}
/// 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<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');
}
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<bool> checkUserSession() async {
try {
var user = await Amplify.Auth.getCurrentUser();
print("checkUserSession");
print(user.signInDetails);
return user != null; // Si el usuario existe, está autenticado
} catch (e) {
return false; // Si hay error, el usuario NO está autenticado
}
}
Future<void> signOut(BuildContext context) async {
try {
await Amplify.Auth.signOut();
print("Sesión cerrada por inactividad.");
// Redirigir al usuario a LoginScreen y limpiar historial de navegación
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => LoginScreen()),
(route) => false,
);
} catch (e) {
print("Error al cerrar sesión: $e");
}
}
}
Como ya se menciona en varias partes, este código esta generado casi en totalidad anteriormente para las funcionalidades que desarrollamos, pero en este caso solo hemos modificado el último método. Vamos a ver que hace.
Future<void>
→ Indica que esta función es asíncrona y no devuelve ningún valor (void
).BuildContext context
→ Se usa para acceder a la navegación y redirigir al usuario después del cierre de sesión.
try {}
→ Bloque de código donde se intenta cerrar la sesión.await Amplify.Auth.signOut();
→ Cierra la sesión en AWS Cognito, eliminando la sesión del usuario en el backend.print("Sesión cerrada por inactividad.");
→ Mensaje de depuración en la consola para confirmar que la sesión se cerró correctamente.
Ahora analicemos:
Navigator.pushAndRemoveUntil()
→ Reemplaza la pantalla actual conLoginScreen
y elimina todo el historial de navegación, evitando que el usuario pueda volver aHomeScreen
.context
→ Es necesario para realizar la navegación en Flutter.MaterialPageRoute(builder: (_) => LoginScreen())
→ Crea una nueva ruta aLoginScreen
.(route) => false
→ Indica que todas las pantallas anteriores deben ser eliminadas del historial.
Esto garantiza que, después de cerrar sesión, el usuario no pueda presionar el botón «atrás» para volver a HomeScreen
.
catch (e) {}
→ Captura cualquier error que pueda ocurrir al cerrar sesión.print("Error al cerrar sesión: $e");
→ Muestra el error en la consola para facilitar la depuración.
Ejemplo de errores posibles:
- Problemas de conexión con AWS Cognito.
- El usuario ya estaba cerrado en AWS, causando un error en
signOut()
.
Resumen del flujo:
- El usuario solicita cerrar sesión.
- Se llama a
Amplify.Auth.signOut()
para cerrar la sesión en AWS Cognito. - Si el cierre es exitoso, se muestra un mensaje en consola.
- El usuario es redirigido a
LoginScreen
y se limpia el historial de navegación. - Si hay un error, se captura y se muestra en la consola.
Resumen
En este artículo, exploramos cómo implementar un cierre automático de sesión por inactividad en una aplicación Flutter con AWS Cognito. Esta funcionalidad es crucial para mejorar la seguridad de la aplicación, asegurando que los usuarios inactivos sean desconectados automáticamente después de un período determinado.
El objetivo principal de este ejercicio es mejorar la seguridad en una aplicación Flutter con AWS Cognito, asegurando que:
- Los usuarios inactivos sean cerrados automáticamente para evitar accesos no autorizados.
- El temporizador se reinicie con cada interacción, evitando cierres de sesión innecesarios.
- El historial de navegación se limpie tras cerrar sesión, evitando que el usuario regrese a la sesión anterior.
Este método es especialmente útil en aplicaciones donde la seguridad y la gestión de sesiones son críticas, como en apps bancarias, de comercio electrónico o sistemas de gestión de usuarios.
Posibles Mejoras para Futuras Implementaciones
- Mostrar una alerta antes de cerrar sesión, avisando al usuario que su sesión está por expirar.
- Permitir que el usuario extienda su sesión manualmente con un botón en la alerta.
- Configurar el tiempo de inactividad desde AWS Cognito, en lugar de definirlo en Flutter.
- Hacer que el temporizador funcione en toda la app, no solo en
HomeScreen
.
n este artículo, implementamos un sistema de cierre de sesión por inactividad en Flutter con AWS Cognito, permitiendo que los usuarios sean desconectados automáticamente tras un tiempo de inactividad. Este mecanismo mejora la seguridad y evita que sesiones abiertas sin uso puedan ser aprovechadas indebidamente.
Si en el futuro queremos mejorar este sistema, podríamos agregar notificaciones antes del cierre de sesión, permitir configuraciones dinámicas de tiempo de inactividad o integrar esta lógica en múltiples pantallas.
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!