Learn how to effectively manage the lifecycle of sessions and JWT tokens. Session management is an important topic that directly affects user experience and security.
Session-based authentication is a method where the server stores the user’s login state on the server side. Understanding how it works helps you better handle security and scalability issues.Session Authentication Flow:
When user logs in, server generates a session ID
Session data (user ID, etc.) is stored in session store
Session ID is sent to client as a cookie
Each subsequent request authenticates user via session ID in cookie
Advantages:
Server has full control over sessions (can invalidate anytime)
Sensitive information is not exposed to client
Simple to implement and well-proven method
Disadvantages:
Stores state on server, requiring scalability considerations
Session store can become a single point of failure (SPOF)
Memory Store (Development Only):Memory store should only be used in development environments. All sessions are lost when the server restarts, and sessions cannot be shared across multiple server instances.
Copy
// server.ts (Development environment)import fastify from "fastify";import fastifySession from "@fastify/session";import fastifyCookie from "@fastify/cookie";const app = fastify();app.register(fastifyCookie);app.register(fastifySession, { secret: process.env.SESSION_SECRET!, cookie: { secure: false, // Allow HTTP in development httpOnly: true, maxAge: 1000 * 60 * 60 * 24, // 1 day }, saveUninitialized: false, // Uses memory store if store is not specified});
Problems with Memory Store:
All sessions lost on server restart
Cannot share sessions across multiple server instances
Memory usage growth can degrade performance
Never use in production
Redis Store (Production):Using Redis as a session store is the industry standard for production environments. Redis is fast, scalable, and supports session sharing across multiple server instances.
// server.ts (Production)import fastify from "fastify";import fastifySession from "@fastify/session";import fastifyCookie from "@fastify/cookie";import fastifyRedis from "@fastify/redis";import RedisStore from "connect-redis";const app = fastify();// Redis connectionapp.register(fastifyRedis, { host: process.env.REDIS_HOST || "localhost", port: parseInt(process.env.REDIS_PORT || "6379"), password: process.env.REDIS_PASSWORD, // Connection pool settings lazyConnect: false, enableReadyCheck: true,});app.register(fastifyCookie);// Use Redis as session storeapp.register(fastifySession, { secret: process.env.SESSION_SECRET!, store: new RedisStore({ client: app.redis, // Use Fastify Redis client prefix: "sess:", // Session key prefix ttl: 60 * 60 * 24 * 7, // 7 days (in seconds) }), cookie: { secure: true, // HTTPS required httpOnly: true, // XSS prevention sameSite: "strict", // CSRF prevention maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days }, saveUninitialized: false, resave: false,});
Redis Configuration Explained:
prefix: Prefix for session keys to distinguish from other data
ttl: Time To Live, automatic session expiration time (in seconds)
client: Redis client instance
Redis supports cluster mode for implementing high availability (HA). Using managed services like AWS ElastiCache or Azure Cache for Redis makes operations more convenient.
PostgreSQL Store (Alternative):If you’re already using PostgreSQL, you can use PostgreSQL as a session store without a separate Redis. However, performance is lower than Redis.
JWT (JSON Web Token) is a stateless authentication method. After the server creates a token, all information is contained within the token itself, so the server doesn’t need to store anything separately.JWT Structure:
Copy
header.payload.signature
Header: Token type and algorithm (e.g., {"alg": "HS256", "typ": "JWT"})
Payload: User information and claims (e.g., {"userId": 123, "role": "user"})
Signature: Signature to verify token integrity
JWT Advantages:
Good scalability as server doesn’t store state
Suitable for microservice environments
No separate lookup needed as information is contained in token itself
JWT Disadvantages:
Difficult to forcibly invalidate token from server
Token size is larger than cookies
If token is stolen, it can be used until expiration
Access Token is a short-lived token used for API requests. Usually has a short validity period of 15 minutes to 1 hour. Even if stolen, damage is minimized.Refresh Token is a long-lived token used to reissue Access Tokens. Usually has a validity period of 7 to 30 days and is stored on the server for management.Why use two tokens?If Access Token validity is set to 30 days:
If stolen, it can be abused for 30 days
User permission changes are not immediately reflected
With short Access Token and Refresh Token renewal:
Stolen Access Token is only valid for a short time
Permission changes can be reflected quickly
Refresh Token can be stored on server and invalidated
CREATE TABLE refresh_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_user_id (user_id), INDEX idx_token (token), INDEX idx_expires_at (expires_at));
class AuthModel extends BaseModelClass { /** * Refresh Access Token * * Receives Refresh Token and issues new Access Token. * Verifies both token validity and database storage. */ @api({ httpMethod: "POST" }) async refreshToken(params: { refreshToken: string; }): Promise<{ accessToken: string; refreshToken?: string; // For Refresh Token Rotation }> { const { refreshToken } = params; // 1. Verify Refresh Token const payload = verifyRefreshToken(refreshToken); if (!payload || typeof payload === "string") { throw new Error("Invalid refresh token"); } // 2. Verify Refresh Token in database const rdb = this.getPuri("r"); const storedToken = await rdb .table("refresh_tokens") .where("token", refreshToken) .where("user_id", payload.userId) .where("expires_at", ">", new Date()) .first(); if (!storedToken) { throw new Error("Refresh token not found or expired"); } // 3. Query user info const user = await rdb .table("users") .where("id", payload.userId) .first(); if (!user) { throw new Error("User not found"); } // 4. Generate new Access Token const accessToken = generateAccessToken(user); // 5. Refresh Token Rotation (optional) // Renew Refresh Token together for enhanced security const newRefreshToken = generateRefreshToken(user); const wdb = this.getPuri("w"); // Delete existing Refresh Token await wdb .table("refresh_tokens") .where("token", refreshToken) .delete(); // Save new Refresh Token await wdb.table("refresh_tokens").insert({ user_id: user.id, token: newRefreshToken, expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }); return { accessToken, refreshToken: newRefreshToken, }; }}
Refresh Token Rotation is a technique that issues a new Refresh Token each time one is used, enhancing security. Even if a token is stolen, only one of the thief or legitimate user can receive a new token, enabling quick detection of theft.