
Building Secure Session-Based Authentication from Scratch
By
D3OXY
By
D3OXY
Session-based authentication remains one of the most reliable and secure methods for managing user authentication in web applications. In this comprehensive guide, we’ll build a secure session-based authentication system from scratch using TypeScript, Express, and Redis.
Before diving into the implementation, let’s understand why session-based authentication is often the better choice:
First, let’s set up our project with the necessary dependencies:
# Initialize project
pnpm init
# Install dependencies
pnpm add express express-session connect-redis redis
pnpm add @types/express @types/express-session @types/connect-redis
pnpm add bcryptjs zod argon2 cors helmet
pnpm add -D typescript @types/node ts-node-dev
Create a tsconfig.json
:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
// src/lib/redis.ts
import { createClient } from "redis";
export const redis = createClient({
url: process.env.REDIS_URL ?? "redis://localhost:6379",
});
redis.on("error", (err) => console.error("Redis Client Error", err));
redis.on("connect", () => console.log("Redis Client Connected"));
await redis.connect();
// src/config/session.ts
import session from "express-session";
import connectRedis from "connect-redis";
import { redis } from "../lib/redis";
const RedisStore = connectRedis(session);
export const sessionConfig = {
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET ?? "super-secret-key",
name: "sid",
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
/**
* Lax is usually a good balance, but feel free to tighten to "strict"
* if your app does not rely on cross-site GET navigation.
*/
sameSite: "lax" as const,
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
},
};
// src/schemas/user.schema.ts
import { z } from "zod";
export const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
});
export const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
});
export type RegisterInput = z.infer<typeof registerSchema>;
export type LoginInput = z.infer<typeof loginSchema>;
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
if (!req.session.userId) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
};
export const isGuest = (req: Request, res: Response, next: NextFunction) => {
if (req.session.userId) {
return res.status(400).json({ error: "Already authenticated" });
}
next();
};
// src/routes/auth.routes.ts
import { Router } from "express";
import * as argon2 from "argon2";
import { loginSchema, registerSchema } from "../schemas/user.schema";
import { isAuthenticated, isGuest } from "../middleware/auth";
import { prisma } from "../lib/prisma";
const router = Router();
router.post("/register", isGuest, async (req, res) => {
try {
const data = registerSchema.parse(req.body);
const existingUser = await prisma.user.findUnique({
where: { email: data.email },
});
if (existingUser) {
return res.status(400).json({ error: "Email already registered" });
}
const hashedPassword = await argon2.hash(data.password);
const user = await prisma.user.create({
data: {
email: data.email,
password: hashedPassword,
name: data.name,
},
});
req.session.userId = user.id;
return res.status(201).json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
});
} catch (error) {
return res.status(400).json({ error: "Invalid input" });
}
});
router.post("/login", isGuest, async (req, res) => {
try {
const data = loginSchema.parse(req.body);
const user = await prisma.user.findUnique({
where: { email: data.email },
});
if (!user) {
return res.status(400).json({ error: "Invalid credentials" });
}
const validPassword = await argon2.verify(user.password, data.password);
if (!validPassword) {
return res.status(400).json({ error: "Invalid credentials" });
}
req.session.userId = user.id;
return res.json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
});
} catch (error) {
return res.status(400).json({ error: "Invalid input" });
}
});
router.post("/logout", isAuthenticated, (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Could not logout" });
}
res.clearCookie("sid");
return res.json({ message: "Logged out successfully" });
});
});
export default router;
// Better password hashing with Argon2
const hashedPassword = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 3,
});
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: { error: 'Too many login attempts. Please try again later.' }
});
router.post('/login', loginLimiter, ...);
import csurf from "csurf";
app.use(
csurf({
cookie: {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
},
})
);
import helmet from "helmet";
app.use(helmet());
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: "Session error" });
req.session.userId = user.id;
});
// In Redis store
await redis.sAdd(`user:${userId}:sessions`, sessionId);
if (req.body.rememberMe) {
req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 30; // 30 days
}
The complete source code for this implementation is available on GitHub: Session-Based Auth Template
Special thanks to:
Session-based authentication provides a robust, secure, and scalable solution for web applications. By following these best practices and implementing proper security measures, you can build a reliable authentication system that protects your users’ data while maintaining a great user experience.
Remember to:
The code examples provided are production-ready but should be adapted to your specific needs and security requirements.