@komarcalabs/auth-session
v0.1.7
Published
<a id="komarcalabsauth-session"></a>
Readme
@komarcalabs/auth-session
Una librería agnóstica y robusta para la gestión de autenticación y sesiones en aplicaciones front-end. Proporciona una API simple y flexible para manejar tokens de acceso, refresh tokens, y estados de sesión.
Características
- 🔐 Gestión completa de tokens: Soporte para access tokens y refresh tokens
- 💾 Múltiples estrategias de almacenamiento: localStorage, sessionStorage, memoria o cookies
- 🔄 Renovación automática: Renovación automática de tokens antes de expirar
- 📡 Sincronización multi-pestaña: Mantiene la sesión sincronizada automáticamente entre pestañas abiertas
- ⚡ Soporte No-JWT: Funciona con tokens JWT y tokens opacos
- 🛡️ Sistema de eventos: Escucha cambios en el estado de autenticación
- ✅ Validación de sesión: Verificación automática de expiración y validación personalizada
- 🎯 TypeScript: Completamente tipado para una mejor experiencia de desarrollo
- 🧪 Probado: Suite completa de tests unitarios
- 🌐 Agnóstico: Funciona con cualquier framework o librería front-end
Instalación
npm install @komarcalabs/auth-session
# o
yarn add @komarcalabs/auth-session
# o
pnpm add @komarcalabs/auth-sessionDocumentación
La documentación completa está organizada en la carpeta docs:
📚 Índice de Documentación
- Instalación
- Configuración básica
- Primeros pasos
- Flujo de autenticación con backend
- Uso básico (login, logout, obtener token)
Ciclo de Vida de la Autenticación
- Entendiendo
getAccessToken(Síncrono vs Asíncrono) - Implementación con Interceptores (Axios/Fetch)
- Renovación automática y expiración
- Entendiendo
- Métodos principales
- Sistema de eventos
- Tipos TypeScript
- Interfaces y tipos exportados
- Opciones de configuración detalladas
- Explicación de cada opción
- Ejemplos de configuración
- ¿Qué es el token decoder?
- ¿Cuándo necesitas un decoder personalizado?
- Casos de uso específicos
- Ejemplos prácticos
- localStorage, sessionStorage, memory, cookie
- Comparación y recomendaciones
- Apps Bancarias
- Multi-Subdominio
- SSR (Next.js)
- Testing
- Servicio de autenticación completo
- Integración con React
- Interceptor de Fetch
- Integración con Axios
- Testing
Inicio Rápido
import { AuthSession } from '@komarcalabs/auth-session';
// Definir tu tipo de User personalizado
interface MyUser {
id: string;
role: 'admin' | 'user';
}
const authSession = new AuthSession<MyUser>({
onRefreshToken: async (refreshToken) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
return response.json();
},
});
// Login
await authSession.login({
accessToken: 'token-del-backend',
refreshToken: 'refresh-token',
expiresIn: 3600,
});
// Obtener token
const token = await authSession.getAccessToken();Seguridad
- Los tokens se almacenan de forma segura usando las APIs nativas del navegador
- La librería no envía tokens automáticamente; tú controlas cuándo y cómo usarlos
- Se recomienda usar HTTPS en producción
- Considera implementar validación adicional en el servidor
- Los refresh tokens deben ser manejados con cuidado y almacenados de forma segura
Licencia
MIT
Contribuir
Las contribuciones son bienvenidas. Por favor, abre un issue o pull request.
Guía de Inicio Rápido
Instalación
npm install @komarcalabs/auth-session
# o
yarn add @komarcalabs/auth-session
# o
pnpm add @komarcalabs/auth-sessionFlujo de Autenticación con Backend
Sí, el JWT lo envía el backend. Esta librería es agnóstica y no hace peticiones HTTP por sí misma. Tu backend es quien:
- Genera los JWTs después de validar credenciales
- Envía los tokens en la respuesta del login
- Renueva los tokens cuando se solicita un refresh
El frontend solo:
- Recibe los tokens del backend
- Los almacena de forma segura
- Los gestiona (renovación automática, validación, etc.)
- Los usa para autenticar peticiones
Ejemplo Completo: Login con Backend
import { AuthSession } from '@komarcalabs/auth-session';
const authSession = new AuthSession({
onRefreshToken: async (refreshToken) => {
// El backend renueva el token
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Failed to refresh token');
}
// El backend devuelve los nuevos tokens
const data = await response.json();
return {
accessToken: data.accessToken, // JWT del backend
refreshToken: data.refreshToken, // Refresh token del backend
expiresIn: data.expiresIn, // Tiempo de expiración
};
},
});
// Función de login que comunica con el backend
async function login(email: string, password: string) {
// 1. Envías credenciales al backend
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
// 2. El backend responde con los tokens (JWT)
const data = await response.json();
// Ejemplo de respuesta del backend:
// {
// accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
// refreshToken: "refresh_token_here",
// expiresIn: 3600,
// user: { id: "123", email: "[email protected]", name: "John" }
// }
// 3. Guardas los tokens en la librería
await authSession.login(
{
accessToken: data.accessToken, // JWT del backend
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
},
data.user // Información del usuario (opcional)
);
}Respuesta Típica del Backend
El backend generalmente responde con algo como:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4",
"expiresIn": 3600,
"user": {
"id": "123",
"email": "[email protected]",
"name": "John Doe",
"roles": ["user"]
}
}Uso Básico
Configuración Inicial
import { AuthSession } from '@komarcalabs/auth-session';
const authSession = new AuthSession({
// Opciones de configuración
storageStrategy: 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
storageKey: 'auth_tokens',
userStorageKey: 'auth_user',
defaultExpiresIn: 3600, // 1 hora en segundos
refreshMargin: 300, // 5 minutos antes de expirar
onRefreshToken: async (refreshToken) => {
// Implementa tu lógica de renovación de token
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
};
},
});Login
// Después de un login exitoso
const tokens = {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'refresh_token_here', // Opcional: solo si tu backend lo envía
expiresIn: 3600, // segundos
};
const user = {
id: '123',
email: '[email protected]',
name: 'Juan Pérez',
roles: ['user', 'admin'],
};
await authSession.login(tokens, user);¿Qué pasa si el backend NO envía refresh token?
El refresh token es opcional. La librería funciona perfectamente sin él, pero con limitaciones:
Comportamiento sin refresh token:
Login funciona normalmente: Puedes hacer login solo con
accessTokenawait authSession.login({ accessToken: 'token', expiresIn: 3600, // Sin refreshToken });getAccessToken()retorna el token mientras sea válido:- Si el token está válido: retorna el token
- Si el token está expirado: retorna
null(no puede renovarlo) - Si el token está cerca de expirar: retorna el token actual (no puede renovarlo preventivamente)
refresh()lanza un error: Si intentas llamarrefresh()sin refresh token, obtendrás un error:try { await authSession.refresh(); // Error: "No refresh token available" } catch (error) { console.error(error); // No se puede renovar sin refresh token }Cuando el token expira: La sesión se marca como
'expired'y necesitas hacer login nuevamente
Ejemplo: Manejo sin refresh token
const authSession = new AuthSession({
// No necesitas onRefreshToken si no usas refresh tokens
});
// Login sin refresh token
await authSession.login({
accessToken: 'token-del-backend',
expiresIn: 3600,
});
// Usar el token mientras sea válido
const token = await authSession.getAccessToken();
if (token) {
// Token válido, usarlo
fetch('/api/protected', {
headers: { Authorization: `Bearer ${token}` },
});
} else {
// Token expirado, redirigir a login
window.location.href = '/login';
}
// Escuchar cuando expire
authSession.on('expired', () => {
// Redirigir a login cuando expire
window.location.href = '/login';
});Recomendación:
- Con refresh token: Sesiones más largas, renovación automática, mejor UX
- Sin refresh token: Sesiones más cortas, el usuario debe hacer login nuevamente cuando expire
Obtener Token de Acceso
// Obtiene el token de acceso, renovándolo automáticamente si es necesario
const accessToken = await authSession.getAccessToken();
if (accessToken) {
// Usa el token para hacer peticiones autenticadas
const response = await fetch('/api/protected', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
}Verificar Autenticación
if (authSession.isAuthenticated()) {
console.log('Usuario autenticado');
const user = authSession.getUser();
console.log('Usuario:', user);
}Logout
await authSession.logout();Renovación Manual de Token
try {
const newTokens = await authSession.refresh();
console.log('Tokens renovados:', newTokens);
} catch (error) {
console.error('Error al renovar token:', error);
// Manejar error, posiblemente redirigir a login
}Referencia de API
Métodos Principales
login(tokens: AuthTokens, user?: User): Promise<void>
Inicia sesión guardando los tokens y opcionalmente la información del usuario.
await authSession.login(
{
accessToken: 'token',
refreshToken: 'refresh',
expiresIn: 3600,
},
{
id: '123',
email: '[email protected]',
}
);logout(): Promise<void>
Cierra la sesión y elimina todos los datos almacenados.
await authSession.logout();getAccessToken(): Promise<string | null>
Obtiene el token de acceso actual. Si el token está expirado o cerca de expirar, intenta renovarlo automáticamente.
const token = await authSession.getAccessToken();refresh(): Promise<AuthTokens>
Renueva el token de acceso usando el refresh token. Evita múltiples llamadas simultáneas.
const newTokens = await authSession.refresh();isAuthenticated(): boolean
Verifica si hay una sesión activa y válida.
if (authSession.isAuthenticated()) {
// Usuario autenticado
}getUser(): User | null
Obtiene la información del usuario actual.
const user = authSession.getUser();updateUser(user: Partial<User>): void
Actualiza parcialmente la información del usuario.
authSession.updateUser({ name: 'Nuevo Nombre' });getSession(): SessionData | null
Obtiene la sesión completa (tokens + usuario).
const session = authSession.getSession();validateSession(): Promise<boolean>
Valida la sesión actual, verificando expiración y ejecutando validadores personalizados.
const isValid = await authSession.validateSession();getStatus(): SessionStatus
Obtiene el estado actual de la sesión: 'authenticated' | 'unauthenticated' | 'expired' | 'refreshing'.
const status = authSession.getStatus();getRefreshToken(): string | null
Obtiene el refresh token actual.
const refreshToken = authSession.getRefreshToken();Sistema de Eventos
Puedes escuchar eventos de autenticación para reaccionar a cambios:
// Escuchar evento de login
const unsubscribe = authSession.on('login', (event) => {
console.log('Usuario inició sesión:', event.data);
});
// Escuchar evento de logout
authSession.on('logout', (event) => {
console.log('Usuario cerró sesión');
});
// Escuchar evento de refresh
authSession.on('refresh', (event) => {
console.log('Token renovado:', event.data);
});
// Escuchar evento de expiración
authSession.on('expired', (event) => {
console.log('Sesión expirada');
// Redirigir a login
});
// Escuchar errores
authSession.on('error', (event) => {
console.error('Error de autenticación:', event.data);
});
// Desregistrar listener
unsubscribe();Métodos de Eventos
on(eventType: AuthEventType, listener: AuthEventListener): () => void
Registra un listener para un tipo de evento. Retorna una función para desregistrar el listener.
off(eventType: AuthEventType, listener: AuthEventListener): void
Elimina un listener específico de un tipo de evento.
removeAllListeners(): void
Elimina todos los listeners registrados.
Tipos de eventos disponibles:
'login': Se inició sesión'logout': Se cerró sesión'refresh': Se renovó el token'expired': La sesión expiró'error': Ocurrió un error
Tipos TypeScript
La librería exporta todos los tipos necesarios:
import type {
AuthSessionOptions,
AuthTokens,
User,
SessionData,
SessionStatus,
StorageStrategy,
AuthEvent,
AuthEventType,
AuthEventListener,
} from '@komarcalabs/auth-session';AuthTokens
interface AuthTokens {
/** Token de acceso (access token) */
accessToken: string;
/** Token de renovación (refresh token) */
refreshToken?: string;
/** Tiempo de expiración del access token en segundos desde la emisión */
expiresIn?: number;
/** Timestamp de cuando se emitió el token */
issuedAt?: number;
}User
interface User {
/** Identificador único del usuario */
id: string | number;
/** Email del usuario */
email?: string;
/** Nombre del usuario */
name?: string;
/** Roles del usuario */
roles?: string[];
/** Datos adicionales del usuario */
[key: string]: unknown;
}SessionData
interface SessionData {
/** Tokens de autenticación */
tokens: AuthTokens;
/** Información del usuario */
user?: User;
/** Datos adicionales de la sesión */
metadata?: Record<string, unknown>;
}SessionStatus
type SessionStatus =
| 'authenticated'
| 'unauthenticated'
| 'expired'
| 'refreshing';StorageStrategy
type StorageStrategy = 'localStorage' | 'sessionStorage' | 'memory';Configuración
Opciones de Configuración
AuthSessionOptions
interface AuthSessionOptions {
// Estrategia de almacenamiento
storageStrategy?: 'localStorage' | 'sessionStorage' | 'memory';
// Claves de almacenamiento
storageKey?: string; // default: 'auth_tokens'
userStorageKey?: string; // default: 'auth_user'
// Configuración de expiración
defaultExpiresIn?: number; // segundos, default: 3600
refreshMargin?: number; // segundos antes de expirar, default: 300
// Decodificador de token personalizado
tokenDecoder?: (token: string) => {
exp?: number;
iat?: number;
[key: string]: unknown;
};
// Validador de expiración personalizado
isTokenExpired?: (
token: string,
issuedAt?: number,
expiresIn?: number
) => boolean;
// Callback para renovar token (opcional: solo necesario si usas refresh tokens)
onRefreshToken?: (refreshToken: string) => Promise<AuthTokens>;
// Validador de sesión personalizado
onValidateSession?: (session: SessionData) => Promise<boolean>;
}Opciones Detalladas
storageStrategy
Estrategia de almacenamiento para los tokens. Ver Estrategias de Almacenamiento para más detalles.
'localStorage'(por defecto): Los tokens persisten entre sesiones'sessionStorage': Los tokens se eliminan al cerrar la pestaña'memory': Los tokens solo existen en memoria (útil para SSR o testing)
storageKey
Clave utilizada para almacenar los tokens en el storage. Por defecto: 'auth_tokens'.
userStorageKey
Clave utilizada para almacenar la información del usuario. Por defecto: 'auth_user'.
defaultExpiresIn
Tiempo de expiración por defecto en segundos si el backend no proporciona expiresIn.
- Por defecto:
3600(1 hora). - Infinito: Pasa
nullo0para que los tokens nunca expiren por tiempo (útil para estrategias donde solo importa la validez del token en el servidor).
refreshMargin
Tiempo en segundos antes de la expiración en el que se intentará renovar el token automáticamente. Por defecto: 300 (5 minutos).
Por ejemplo, si un token expira en 1 hora y refreshMargin es 5 minutos, el token se renovará automáticamente cuando queden 5 minutos o menos antes de expirar.
tokenDecoder
Función personalizada para decodificar el JWT y extraer información como exp (expiración) e iat (emitido en).
Por defecto, la librería incluye un decodificador básico que funciona con JWTs estándar. Solo necesitas proporcionar un decodificador personalizado si:
- Tu backend usa un formato de token no estándar
- Necesitas extraer información adicional del token
- Quieres usar una librería específica como
jwt-decode
Ver Token Decoder para más detalles y casos de uso.
isTokenExpired
Función personalizada para validar si un token está expirado. Por defecto, la librería usa la información del tokenDecoder o los parámetros issuedAt y expiresIn.
Solo necesitas proporcionar esta función si:
- Tu lógica de expiración es diferente a la estándar
- Necesitas validar expiración basada en otros criterios
onRefreshToken
Callback que se ejecuta cuando se necesita renovar el token. Opcional: solo necesario si usas refresh tokens.
Esta función debe:
- Recibir el
refreshTokencomo parámetro - Hacer una petición al backend para obtener nuevos tokens
- Retornar un objeto
AuthTokenscon los nuevos tokens
onRefreshToken: async (refreshToken: string) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Failed to refresh token');
}
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
};
};onValidateSession
Validador personalizado de sesión. Se ejecuta cuando llamas a validateSession().
onValidateSession: async (session: SessionData) => {
// Verificar con el servidor que la sesión sigue siendo válida
try {
const response = await fetch('/api/auth/validate', {
headers: {
Authorization: `Bearer ${session.tokens.accessToken}`,
},
});
return response.ok;
} catch {
return false;
}
};Sesiones Infinitas (Sin Expiración)
Si deseas que la sesión nunca expire automáticamente por parte del cliente (dejando la validación puramente al servidor o hasta que el usuario haga logout explícito), puedes configurar expiración infinita.
Opción A: Configuración Global
const authSession = new AuthSession({
// ...
defaultExpiresIn: null, // O usa 0
});Opción B: Por Login
await authSession.login({
accessToken: '...',
expiresIn: null, // El token específico no expirará
});Ejemplo de Configuración Completa
import { AuthSession } from '@komarcalabs/auth-session';
import jwtDecode from 'jwt-decode';
const authSession = new AuthSession({
storageStrategy: 'localStorage',
storageKey: 'my_app_auth_tokens',
userStorageKey: 'my_app_user',
defaultExpiresIn: 7200, // 2 horas
refreshMargin: 600, // 10 minutos antes de expirar
tokenDecoder: (token) => {
return jwtDecode(token);
},
onRefreshToken: async (refreshToken) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
};
},
onValidateSession: async (session) => {
// Validación personalizada
return true;
},
});Token Decoder
¿Qué es el Token Decoder?
El tokenDecoder es una función opcional que permite personalizar cómo se decodifica el JWT para extraer información como la fecha de expiración (exp) y la fecha de emisión (iat).
¿Cuándo necesitas un Token Decoder personalizado?
La librería incluye un decodificador básico por defecto que funciona con la mayoría de los JWTs estándar. Solo necesitas proporcionar un decodificador personalizado en los siguientes casos:
1. Usar una librería específica de decodificación
Si prefieres usar una librería como jwt-decode que puede manejar mejor casos edge o proporcionar mejor validación:
import jwtDecode from 'jwt-decode';
const authSession = new AuthSession({
tokenDecoder: (token) => {
return jwtDecode(token);
},
});Ventajas:
- Mejor manejo de errores
- Validación más robusta
- Soporte para diferentes formatos de JWT
2. Tokens con formato no estándar
Si tu backend genera tokens que no siguen el formato JWT estándar (aunque esto es raro):
const authSession = new AuthSession({
tokenDecoder: (token) => {
// Tu lógica personalizada para decodificar
try {
// Ejemplo: token en formato base64 custom
const decoded = JSON.parse(atob(token));
return {
exp: decoded.expiration,
iat: decoded.issued_at,
};
} catch {
return {};
}
},
});3. Extraer información adicional del token
Si necesitas acceder a información adicional del payload del JWT (como roles, permisos, etc.):
import jwtDecode from 'jwt-decode';
const authSession = new AuthSession({
tokenDecoder: (token) => {
const decoded = jwtDecode(token);
// Ahora puedes acceder a decoded.roles, decoded.permissions, etc.
return decoded;
},
});
// Más tarde, puedes decodificar el token para obtener información adicional
const session = authSession.getSession();
if (session) {
// Guarda una referencia al decoder si necesitas usarlo después
const tokenDecoder = (token: string) => {
return jwtDecode(token);
};
const decoded = tokenDecoder(session.tokens.accessToken);
console.log('Roles:', decoded.roles);
console.log('Permissions:', decoded.permissions);
}4. Validación adicional durante la decodificación
Si necesitas validar el token durante la decodificación:
const authSession = new AuthSession({
tokenDecoder: (token) => {
try {
const decoded = jwtDecode(token);
// Validaciones adicionales
if (!decoded.sub) {
throw new Error('Token missing subject');
}
if (decoded.iss !== 'https://my-auth-server.com') {
throw new Error('Invalid token issuer');
}
return decoded;
} catch (error) {
console.error('Token decode error:', error);
return {};
}
},
});5. Manejo de errores personalizado
Si quieres un manejo de errores más específico:
const authSession = new AuthSession({
tokenDecoder: (token) => {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid token format');
}
const payload = JSON.parse(atob(parts[1]));
return payload;
} catch (error) {
// Log para debugging
console.error('Failed to decode token:', error);
// Retornar objeto vacío para que la librería use expiresIn/issuedAt
return {};
}
},
});Comportamiento por Defecto
Si no proporcionas un tokenDecoder, la librería usa un decodificador básico que:
- Divide el token por
.(formato JWT estándar:header.payload.signature) - Decodifica el payload (segunda parte) desde base64
- Extrae
expeiatdel payload decodificado - Retorna un objeto vacío si hay algún error
// Decodificador por defecto (simplificado)
function defaultTokenDecoder(token: string) {
try {
const parts = token.split('.');
if (parts.length !== 3) {
return {};
}
const payload = parts[1];
const decoded = JSON.parse(
atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
);
return decoded;
} catch {
return {};
}
}¿Cuándo NO necesitas un Token Decoder personalizado?
No necesitas un decodificador personalizado si:
- ✅ Tu backend genera JWTs estándar
- ✅ El backend siempre envía
expiresInen la respuesta del login - ✅ No necesitas acceder a información adicional del token
- ✅ El decodificador por defecto funciona correctamente
En estos casos, simplemente no proporciones tokenDecoder y la librería usará el decodificador por defecto.
Ejemplo Completo
import { AuthSession } from '@komarcalabs/auth-session';
import jwtDecode from 'jwt-decode';
const authSession = new AuthSession({
// Usar jwt-decode para mejor manejo de errores
tokenDecoder: (token) => {
try {
return jwtDecode(token);
} catch (error) {
console.error('Error decoding token:', error);
return {};
}
},
onRefreshToken: async (refreshToken) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
};
},
});
// Acceder a información adicional del token
function getUserRoles(): string[] {
const session = authSession.getSession();
if (!session) return [];
const decoded = authSession.options.tokenDecoder(session.tokens.accessToken);
return decoded.roles || [];
}Resumen
| Caso | ¿Necesitas tokenDecoder? |
| ----------------------------------------------- | ------------------------- |
| JWT estándar con expiresIn del backend | ❌ No |
| JWT estándar sin expiresIn | ❌ No (usa exp del JWT) |
| Quieres usar jwt-decode | ✅ Opcional (recomendado) |
| Token con formato no estándar | ✅ Sí |
| Necesitas extraer información adicional | ✅ Sí |
| Validación personalizada durante decodificación | ✅ Sí |
Estrategias de Almacenamiento
La librería soporta tres estrategias diferentes para almacenar los tokens de autenticación. Cada una tiene sus ventajas y casos de uso específicos.
localStorage (por defecto)
Los tokens persisten entre sesiones del navegador. Se mantienen incluso después de cerrar y abrir el navegador.
Características:
- ✅ Persistencia entre sesiones
- ✅ Los tokens sobreviven a reinicios del navegador
- ✅ Ideal para aplicaciones que quieren mantener al usuario logueado
- ⚠️ Los tokens persisten hasta que se eliminen explícitamente o el usuario limpie el almacenamiento
Uso:
const authSession = new AuthSession({
storageStrategy: 'localStorage',
});Casos de uso:
- Aplicaciones web tradicionales
- Cuando quieres que el usuario permanezca logueado entre sesiones
- Aplicaciones donde la seguridad no es crítica (los tokens pueden ser accesibles por JavaScript)
sessionStorage
Los tokens se eliminan automáticamente al cerrar la pestaña o ventana del navegador.
Características:
- ✅ Más seguro que localStorage (se elimina al cerrar la pestaña)
- ✅ Los tokens no persisten entre sesiones
- ✅ Ideal para aplicaciones que requieren re-autenticación frecuente
- ⚠️ Si el usuario abre múltiples pestañas, cada una tiene su propia sesión
Uso:
const authSession = new AuthSession({
storageStrategy: 'sessionStorage',
});Casos de uso:
- Aplicaciones bancarias o financieras
- Aplicaciones con datos sensibles
- Cuando quieres forzar logout al cerrar la pestaña
- Aplicaciones donde la seguridad es crítica
memory
Los tokens solo existen en memoria (RAM). Se pierden al recargar la página o cerrar la aplicación.
Características:
- ✅ Máxima seguridad (no se persiste en disco)
- ✅ Útil para SSR (Server-Side Rendering)
- ✅ Ideal para testing
- ⚠️ Los tokens se pierden al recargar la página
- ⚠️ No persiste entre navegaciones
Uso:
const authSession = new AuthSession({
storageStrategy: 'memory',
});Casos de uso:
Testing y desarrollo
Aplicaciones SSR (Next.js, Nuxt, etc.)
Cuando necesitas evitar almacenamiento persistente
Aplicaciones que requieren autenticación en cada carga
Aplicaciones que requieren autenticación en cada carga
cookie
Almacena los tokens en document.cookie. Permite que el servidor (SSR) acceda a los tokens y compartir sesión entre subdominios.
Características:
- ✅ Soporte para SSR (Next.js, etc.)
- ✅ Permite compartir sesión entre subdominios (
.ejemplo.com) - ⚠️ Tamaño limitado (4KB)
- ⚠️ Se envía en cada petición HTTP al servidor (overhead)
Uso:
const authSession = new AuthSession({
storageStrategy: 'cookie',
cookieOptions: {
domain: '.mi-sitio.com', // Compartir entre subdominios
secure: true, // Solo HTTPS
sameSite: 'Lax',
},
});Casos de uso:
- Renderizado del lado del servidor (SSR)
- Compartir login entre
app.dominio.comyblog.dominio.com
Comparación
| Característica | localStorage | sessionStorage | memory | cookie | | --------------------------- | -------------- | -------------------- | ------- | --------------- | | Persistencia entre sesiones | ✅ Sí | ❌ No | ❌ No | ✅ Sí | | Persistencia entre pestañas | ✅ Sí | ✅ Sí (misma sesión) | ❌ No | ✅ Sí | | Soporte Subdominios | ❌ No | ❌ No | ❌ No | ✅ Sí | | Acceso Servidor (SSR) | ❌ No | ❌ No | ❌ No | ✅ Sí | | Casos de uso | Apps generales | Apps sensibles | Testing | SSR/Subdominios |
Cambiar de Estrategia
Puedes cambiar la estrategia de almacenamiento en cualquier momento creando una nueva instancia:
// Cambiar de localStorage a sessionStorage
const authSession = new AuthSession({
storageStrategy: 'sessionStorage',
});Nota: Si cambias la estrategia, los tokens almacenados con la estrategia anterior no serán accesibles. El usuario necesitará hacer login nuevamente.
Ejemplo: Selección Dinámica
// Seleccionar estrategia basada en el entorno
const storageStrategy =
process.env.NODE_ENV === 'test'
? 'memory'
: process.env.NODE_ENV === 'development'
? 'sessionStorage'
: 'localStorage';
const authSession = new AuthSession({
storageStrategy,
});Recomendaciones
Para Producción:
- Aplicaciones generales: Usa
localStoragepara mejor UX - Aplicaciones con datos sensibles: Usa
sessionStoragepara mayor seguridad - Aplicaciones bancarias/financieras: Considera
sessionStorageo inclusomemorycon re-autenticación frecuente
Para Desarrollo:
- Usa
memoryosessionStoragepara evitar tokens persistentes durante el desarrollo
Para Testing:
- Siempre usa
memorypara evitar interferencias entre tests
describe('Auth tests', () => {
it('should login', async () => {
const authSession = new AuthSession({
storageStrategy: 'memory', // Aislar cada test
});
// ...
});
});Ejemplos de Configuración de Estrategias
Este documento proporciona ejemplos prácticos de cómo configurar @komarcalabs/auth-session para diferentes tipos de aplicaciones y requisitos de negocio.
Escenario 1: Aplicación Web Estándar (SPA/SaaS)
Objetivo: Mantener al usuario logueado indefinidamente (hasta que haga logout) para una buena experiencia de usuario.
// Configuración por defecto: usa localStorage
const authSession = new AuthSession({
storageStrategy: 'localStorage',
// Configurar expiración larga por defecto si el backend lo permite
defaultExpiresIn: 24 * 60 * 60, // 24 horas
});Escenario 2: Aplicación Bancaria o de Alta Seguridad
Objetivo: La sesión debe cerrarse inmediatamente si el usuario cierra la pestaña.
const bankingSession = new AuthSession({
storageStrategy: 'sessionStorage',
// Margen de seguridad para renovar tokens antes
refreshMargin: 60, // 1 minuto antes
});Escenario 3: Plataforma Multi-Subdominio (SaaS)
Objetivo: El usuario hace login en app.empresa.com y mantiene la sesión en dashboard.empresa.com y ayuda.empresa.com.
Solución: Usar cookie con configuración de dominio wildcard.
const platformSession = new AuthSession({
storageStrategy: 'cookie',
cookieOptions: {
domain: '.empresa.com', // El punto al inicio permite compartir con subdominios
path: '/', // Disponible en toda la ruta
secure: true, // Solo HTTPS (Recomendado)
sameSite: 'Lax', // Protección CSRF balanceada
},
});Escenario 4: Server Side Rendering (Next.js)
Objetivo: Que el token esté disponible tanto en el servidor (para renderizado inicial) como en el cliente.
// utils/auth.ts
export const authSession = new AuthSession({
storageStrategy: 'cookie', // Las cookies se envían automáticamente al servidor
cookieOptions: {
sameSite: 'Lax',
path: '/',
},
});Escenario 5: Entorno de Pruebas (Unit Testing)
Objetivo: Aislar cada test para que no compartan estado.
// test/setup.ts
import { AuthSession } from '@komarcalabs/auth-session';
beforeEach(() => {
// Crear una instancia nueva y limpia para cada test
const testSession = new AuthSession({
storageStrategy: 'memory',
});
});Ejemplos de Uso
Ejemplo Completo: Servicio de Autenticación
Aquí tienes un ejemplo completo de cómo integrar la librería con tu backend:
import { AuthSession } from '@komarcalabs/auth-session';
import type { AuthTokens, User } from '@komarcalabs/auth-session';
class AuthService {
private authSession: AuthSession;
private apiBaseUrl: string;
constructor(apiBaseUrl: string) {
this.apiBaseUrl = apiBaseUrl;
// Usamos el tipo User genérico para mayor seguridad
this.authSession = new AuthSession<User>({
storageStrategy: 'localStorage',
onRefreshToken: async (refreshToken) => {
// El backend renueva el token
return this.refreshTokenFromBackend(refreshToken);
},
});
}
/**
* Login: El backend valida credenciales y devuelve JWT
*/
async login(email: string, password: string): Promise<void> {
const response = await fetch(`${this.apiBaseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
// El backend envía los tokens (JWT)
const data = await response.json();
// {
// accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
// refreshToken: "refresh_token_string",
// expiresIn: 3600,
// user: { id: "123", email: "[email protected]", ... }
// }
await this.authSession.login(
{
accessToken: data.accessToken, // JWT del backend
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
},
data.user
);
}
/**
* Refresh: Solicita nuevos tokens al backend
*/
private async refreshTokenFromBackend(
refreshToken: string
): Promise<AuthTokens> {
const response = await fetch(`${this.apiBaseUrl}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Failed to refresh token');
}
// El backend devuelve nuevos tokens
const data = await response.json();
return {
accessToken: data.accessToken, // Nuevo JWT del backend
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
};
}
/**
* Logout: Limpia la sesión local
*/
async logout(): Promise<void> {
// Opcional: notificar al backend
try {
const token = await this.authSession.getAccessToken();
if (token) {
await fetch(`${this.apiBaseUrl}/auth/logout`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
});
}
} catch {
// Ignorar errores de logout en backend
}
await this.authSession.logout();
}
/**
* Obtener token para usar en peticiones
*/
async getAccessToken(): Promise<string | null> {
return this.authSession.getAccessToken();
}
/**
* Verificar si está autenticado
*/
isAuthenticated(): boolean {
return this.authSession.isAuthenticated();
}
/**
* Obtener usuario
*/
getUser(): User | null {
return this.authSession.getUser();
}
}
// Uso
const authService = new AuthService('https://api.example.com');
// Login
await authService.login('[email protected]', 'password');
// Hacer peticiones autenticadas
const token = await authService.getAccessToken();
const response = await fetch('https://api.example.com/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});Integración con React
import { useEffect, useState } from 'react';
import { AuthSession } from '@komarcalabs/auth-session';
const authSession = new AuthSession({
onRefreshToken: async (refreshToken) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
return response.json();
},
});
function useAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(
authSession.isAuthenticated()
);
const [user, setUser] = useState(authSession.getUser());
useEffect(() => {
const unsubscribeLogin = authSession.on('login', () => {
setIsAuthenticated(true);
setUser(authSession.getUser());
});
const unsubscribeLogout = authSession.on('logout', () => {
setIsAuthenticated(false);
setUser(null);
});
const unsubscribeExpired = authSession.on('expired', () => {
setIsAuthenticated(false);
setUser(null);
// Opcional: redirigir a login
window.location.href = '/login';
});
return () => {
unsubscribeLogin();
unsubscribeLogout();
unsubscribeExpired();
};
}, []);
return { isAuthenticated, user, authSession };
}
// Uso en componente
function App() {
const { isAuthenticated, user, authSession } = useAuth();
if (!isAuthenticated) {
return <LoginForm />;
}
return (
<div>
<h1>Bienvenido, {user?.name}</h1>
<button onClick={() => authSession.logout()}>Cerrar Sesión</button>
</div>
);
}Interceptor de Fetch con Renovación Automática
const authSession = new AuthSession({
onRefreshToken: async (refreshToken) => {
// Tu lógica de refresh
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
return response.json();
},
});
async function authenticatedFetch(url: string, options: RequestInit = {}) {
const token = await authSession.getAccessToken();
if (!token) {
throw new Error('No authentication token available');
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
// Si el token expiró, intentar renovar y reintentar
if (response.status === 401) {
try {
await authSession.refresh();
const newToken = await authSession.getAccessToken();
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${newToken}`,
},
});
} catch {
// Si falla el refresh, redirigir a login
await authSession.logout();
window.location.href = '/login';
throw new Error('Session expired');
}
}
return response;
}Validación Personalizada de Sesión
const authSession = new AuthSession({
onValidateSession: async (session) => {
// Verificar con el servidor que la sesión sigue siendo válida
try {
const response = await fetch('/api/auth/validate', {
headers: {
Authorization: `Bearer ${session.tokens.accessToken}`,
},
});
return response.ok;
} catch {
return false;
}
},
});
// Validar periódicamente
setInterval(async () => {
const isValid = await authSession.validateSession();
if (!isValid) {
// Redirigir a login
window.location.href = '/login';
}
}, 5 * 60 * 1000); // Cada 5 minutosDecodificador de JWT Personalizado
import jwtDecode from 'jwt-decode';
const authSession = new AuthSession({
tokenDecoder: (token) => {
return jwtDecode(token);
},
onRefreshToken: async (refreshToken) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
return response.json();
},
});
// Acceder a información adicional del token
// Guarda una referencia al decoder para reutilizarlo
const tokenDecoder = (token: string) => jwtDecode(token);
const authSession = new AuthSession({
tokenDecoder,
onRefreshToken: async (refreshToken) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
});
return response.json();
},
});
function getUserRoles(): string[] {
const session = authSession.getSession();
if (!session) return [];
const decoded = tokenDecoder(session.tokens.accessToken);
return decoded.roles || [];
}Integración con Axios
import axios from 'axios';
import { AuthSession } from '@komarcalabs/auth-session';
const authSession = new AuthSession({
onRefreshToken: async (refreshToken) => {
const response = await axios.post('/api/auth/refresh', { refreshToken });
return response.data;
},
});
// Interceptor de axios
axios.interceptors.request.use(async (config) => {
const token = await authSession.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Interceptor de respuesta para manejar 401
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
try {
await authSession.refresh();
// Reintentar la petición original
const token = await authSession.getAccessToken();
error.config.headers.Authorization = `Bearer ${token}`;
return axios.request(error.config);
} catch {
await authSession.logout();
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);Testing
import { AuthSession } from '@komarcalabs/auth-session';
});
});
## Uso con Tokens No-JWT (Opacos)
La librería soporta tokens que no son JWT (por ejemplo, strings aleatorios de sesión). El decodificador por defecto no fallará. Solo asegúrate de proporcionar `expiresIn` al hacer login.
```typescript
// No necesitas configuración especial, el decoder por defecto manejará strings simples retornando {}
const authSession = new AuthSession({
// ... configuración estándar
});
await authSession.login({
accessToken: 'session_id_aleatorio_7382',
refreshToken: 'refresh_token_aleatorio_9921',
expiresIn: 3600, // IMPORTANTE: Definir tiempo de expiración explícitamente
});Ciclo de Vida de la Autenticación
Este documento explica paso a paso el flujo de trabajo recomendado al usar @komarcalabs/auth-session, aclarando cómo y cuándo interactuar con la librería.
1. Login (Inicio de Sesión)
El ciclo comienza cuando el usuario se autentica en tu aplicación.
Lo que tú haces:
- Recolectas credenciales (usuario/password) en tu UI.
- Las envías a tu backend.
- Recibes los tokens (accessToken, refreshToken).
- Se los pasas a la librería.
// Tu función de login
async function handleLogin(credentials) {
// 1. Petición al backend
const response = await api.post('/login', credentials);
const { accessToken, refreshToken, expiresIn, user } = response.data;
// 2. Guardar en la librería (Síncrono)
await authSession.login({ accessToken, refreshToken, expiresIn }, user);
// 3. Redirigir al usuario
router.push('/dashboard');
}2. Consumo de APIs (El Interceptor)
Aquí es donde ocurre la magia. No deberías estar verificando tokens manualmente en cada componente. En su lugar, usa un "Interceptor" en tu cliente HTTP (Axios, Fetch, etc.).
¿Por qué getAccessToken() es asíncrono?
Puede parecer extraño tener que hacer await auth.getAccessToken(), pero es una decisión de diseño crítica por eficiencia y seguridad:
- Eficiencia: La función primero verifica en memoria si el token es válido. Esto toma microsegundos.
- Seguridad: Si (y solo si) el token está por vencer, la librería pausa la ejecución, renueva el token en segundo plano, y luego te devuelve el nuevo.
¿Es costoso? ¡No! En el 99% de los casos, la función retorna inmediatamente el token guardado. Solo se hace lenta cuando realmente necesita renovar el token.
Ejemplo con Axios
Este es el patrón estándar de oro. Configúralo una vez y olvídate.
import axios from 'axios';
import { authSession } from './auth';
const api = axios.create({ baseURL: '/api' });
api.interceptors.request.use(async (config) => {
// 1. Obtener token (Automáticamente maneja renovación si es necesario)
const token = await authSession.getAccessToken();
// 2. Inyectar en headers
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});Ejemplo con Fetch
Si usas fetch, puedes crear un wrapper simple:
async function authFetch(url, options = {}) {
// 1. Asegurar token
const token = await authSession.getAccessToken();
// 2. Preparar headers
const headers = {
...options.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
// 3. Ejecutar
return fetch(url, { ...options, headers });
}3. Renovación Automática (Auto-Refresh)
La librería maneja esto internamente, pero es útil entender qué pasa.
- La librería detecta que el token expirará pronto (basado en
refreshMargin). - Llama a tu callback
onRefreshToken. - Si hay 10 peticiones simultáneas, la librería solo hace 1 refresh y comparte el resultado con todas.
- Si el refresh falla (ej. refresh token inválido), el estado pasa a
expiredounauthenticated.
4. Expiración de Sesión
Si el refresh falla o el token expira irremediablemente, la sesión "muere". Debes escuchar el evento para redirigir al usuario al login.
// En tu archivo principal (main.ts / App.tsx)
authSession.on('expired', () => {
console.warn('La sesión expiró. Redirigiendo al login...');
router.push('/login');
});Nota sobre Sesiones Infinitas: Si configuras
expiresIn: null(o0), este evento nunca se disparará automáticamente por tiempo. Solo se disparará si el servidor rechaza un token (ej.401 Unauthorized) y tú o la librería lo marcan explícitamente como expirado.
5. Logout
Cuando el usuario decide salir explícitamente.
async function handleLogout() {
await authSession.logout();
router.push('/login');
}Resumen de Mejores Prácticas
- Siempre usa
getAccessToken()antes de cada petición protegida. - Centraliza esa lógica en un interceptor o wrapper HTTP.
- Configura
onRefreshTokensi tu backend soporta refresh tokens. - Escucha el evento
expiredpara manejar desconexiones forzadas.
