Are You Using the Wrong Authentication Method? A Node.js Developer’s Guide (Part 1)


Explore Modern Authentication Strategies with Practical Insights and Real-World Examples (Part 1: Session-Based Auth)
Introduction
Among all other things in the world of web, authentication is one of those things you can’t ignore even if you want to. It’s important enough to take it seriously, because it is not only mechanism that protects your app from unauthorized access but also data breaches and potential security concerns. This is why it's important to know how authentication works— it’s not just a feature; it’s the foundation of trust between the app and its users.
Now, there are several ways to authenticate users, each with its own strengths and weaknesses. In this series, we’ll be exploring six major authentication types:
Session-based Authentication (today’s focus)
JWT Authentication (JSON Web Tokens)
OAuth 2.0
API Key Authentication
Multi-factor Authentication (MFA)
Passwordless Authentication
But why should you care about all this? Well, if you’re a developer, understanding authentication is essential for building secure and reliable applications. If you’re a user, knowing how authentication works can help you make informed decisions about your online security and privacy. By the end of this series, you’ll have a solid grasp of the different authentication methods, their pros and cons, and when to use each one. You will also be able to choose the best authentication method according to your applications need.
In this first part, we’re diving deep into Session-based Authentication, a classic and widely used method. Let’s get started!
Session-Based Authentication: A Closer Look
Session-based authentication, often referred to as cookie-based authentication, is like getting a temporary pass after showing your ticket at an event.
Let’s break it down step by step:
Login: First, you present your “ticket” (credentials like username and password) to the server.
Verification: The server acts like security, checking its records to make sure your credentials are legit.
Session Creation: If all goes well, the server creates a unique “session” for you. This session is a temporary storage space on the server, holding details about your visit — kind of like your profile at the event.
Session ID: Along with your session, the server generates a unique identifier — a “session ID” — which is like the code printed on your wristband.
Cookie: The session ID is sent to your browser as a “cookie” (a tiny text file). Your browser keeps this cookie safe and ready to present when needed.
Subsequent Requests: Every time you interact with the server — whether it’s loading a page or submitting a form — your browser automatically includes this cookie with the session ID in the request.
Session Lookup: The server checks the session ID from the cookie against its stored sessions. If it matches, the server knows you’re authenticated and grants you access to the resource.
Logout: When you’re done (or your session times out), the server tears up your metaphorical wristband by destroying the session. The cookie on your browser either gets deleted or is marked as expired.
Creating a basic session-based authentication using Express.js, Prisma, and PostgreSQL (Supabase)
Prerequisites
Before starting, ensure Prisma is installed and set up. We won’t dive into Prisma setup in detail, as this guide focuses specifically on session-based authentication.
Here’s an example Prisma schema for managing users and sessions:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
username String @unique
password String
sessions Session[]
}
model Session {
id String @id @default(uuid())
sid String @unique
data String
expiresAt DateTime
user User @relation(fields: [userId], references: [id])
userId String
}
Create a db.js file to initialize and manage the Prisma connection:
// db.js
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function connectDB() {
try {
await prisma.$connect();
console.log("Database connected successfully!");
} catch (error) {
console.error("Error connecting to the database:", error);
process.exit(1);
}
}
export { prisma, connectDB };
Setting Up the Express.js Server
Create a simple Express.js server in index.js:
// index.js
import express from "express";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Environment Variable
Create a .env file and add a SESSION_SECRET variable. This secret is used to secure sessions. Keep it confidential:
SESSION_SECRET=my_secret_session
Configuring Sessions with express-session
To enable session management, configure the session middleware in your Express app:
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24,
},
})
);
Session Options
resave: Set to false to avoid saving unmodified sessions to the store, reducing unnecessary storage operations.
saveUninitialized: Set to false to prevent saving empty sessions (e.g., when users just visit the site without interacting).
cookie:
secure:Ensures the cookie is sent only over HTTPS in production.
httpOnly:Prevents client-side JavaScript from accessing cookies.
maxAge: Defines the session duration before expiration (1 day in this case).
Here’s the full server setup:
// index.js
import express from "express";
import session from "express-session";
import routes from "./routes/index.js";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24,
},
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/api", routes);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Helper Functions for Session Management
Now, let’s create utility functions to manage sessions in the utils/session.util.js file:
// utils/session.util.js
import { prisma } from "../config/db.js";
// Helper function to create a new session
async function createSession(userId, sid) {
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24); // session will expire after one day
const sessionData = JSON.stringify({ userId });
await prisma.session.create({
data: {
sid,
data: sessionData,
expiresAt,
user: { connect: { id: userId } },
},
});
}
// Helper function to get the user ID from a session ID
async function getUserIdFromSession(sid) {
if (!sid) {
return null;
}
const session = await prisma.session.findUnique({
where: { sid },
include: { user: true },
});
if (session && session.expiresAt > new Date()) {
const sessionData = JSON.parse(session.data);
return sessionData.userId;
}
return null;
}
export { createSession, getUserIdFromSession };
Explanation of the Helper Functions
1. createSession(userId, sid)
Creates a session entry in the database with a unique session ID (sid) and associates it with a user.
Sets the session to expire in 1 day.
2. getUserIdFromSession(sid)
Retrieves the session from the database using the session ID.
Checks if the session is valid (not expired).
Returns the user ID if the session is active; otherwise, returns null.
Now, we will implement the login functionality to generate a session cookie and store it in both the browser for authentication and in the PostgreSQL database for session management.
// controllers/auth.controller.js
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcrypt";
import { v4 as uuidv4 } from "uuid";
import { createSession } from "../utils/session.utils.js";
async function login(req, res) {
const { username, password } = req.body;
try {
const user = await prisma.user.findUnique({
where: { username },
});
if (user && (await bcrypt.compare(password, user.password))) {
const sessionId = uuidv4();
await createSession(user.id, sessionId);
req.session.sid = sessionId;
const { password, ...userWithoutPassword } = user;
res.send({
message: "Logged in successfully!",
data: userWithoutPassword,
});
} else {
res.status(401).send("Invalid credentials.");
}
} catch (error) {
console.error(error);
res.status(500).send("Error logging in.");
}
}
Next, we need to set up the routes for the login API. This will allow us to handle user login requests.
// routes/auth.route.js
import express from "express";
import {
login,
} from "../controllers/auth.controller.js";
const router = express.Router();
router.post("/login", login);
export default router;
To use the auth routes, we import and use them in our main routing file. This will enable the login functionality to be part of the overall application routes.
// routes/index.js
import express from "express";
import authRoutes from "./auth.route.js";
const router = express.Router();
router.use("/auth", authRoutes);
export default router;
Once the routes are set up, hitting the login API will return a success message and set a session cookie in the browser, like so:
connect.sid=s%3AbeCUJyoN_1OOrgP9l8UbrGxDglZkFFTX.ED83IRS8nte5idk53TTZfscSZOkNDrkezZ1IRznjaIw; Path=/; HttpOnly; Expires=Mon, 23 Dec 2024 20:57:12 GMT;
The session cookie (connect.sid) is crucial for maintaining the user's authentication state. It is stored as an HTTP-only cookie, which enhances security by preventing client-side JavaScript from accessing the session ID.
Variations of Session Storage:
While cookies are the most common way to transmit session IDs, there are other less common methods:
URL Rewriting: Imagine the session ID being tacked onto the end of every URL as a query parameter — like https://example.com/home?sessionId=12345. While it might sound straightforward, this method has many risks. The session ID becomes visible in the browser’s address bar, making it vulnerable to exposure through bookmarks or logs. Plus, it’s just not user-friendly.
Hidden Form Fields: The session ID can be included as a hidden field in HTML forms. This is also less common and can be to manage.
Session-based authentication is dependent on the server maintaining a “state” of active sessions. That is why it is also known as a “Stateful” authentication method. The client (your browser) does not conduct any heavy lifting; it merely displays the session ID as verification of its continued connection with the server. Whether delivered via cookies, URLs, or hidden fields, the premise remains the same: keep the session ID secure and your app will stay secure.
Pros and Cons of Session-Based Authentication
Advantages:
Simplicity: Easy to build, especially with web frameworks that include built-in session management.
Server-Side Control: The server handles sessions, allowing you to easily terminate or update them, as well as integrate functionality like “remember me.”
Security (When Done Right): Session IDs are randomly generated and difficult to guess, and sensitive data is securely saved on the server rather than the browser.
Statefulness: The server monitors user activity to provide personalised experiences and features such as persistent shopping carts.
Disadvantages:
Scalability: Storing sessions on a single server might create bottlenecks, that requires distributed session stores such as Redis or load balancing to handle big user bases.
Complex Scaling: Maintaining session consistency across multiple servers complicates horizontal scaling.
Single Point of Failure: If the server storing sessions fails, active sessions are lost, forcing re-authentication. However, distributed session stores serve to lessen this danger.
CSRF Vulnerability: More prone to CSRF attacks unless properly taken care of with tokens or other protections.
When to Use and When to Avoid Session-Based Authentication
Ideal Use Cases:
Traditional Web Applications: Best for monolithic web apps where users interact with a single server or a cluster sharing session data, like e-commerce platforms, CMS, and SaaS apps.
Applications Requiring Server-Side Session Management: Ideal for apps that need to track user activity, store session data securely, or allow for remote session management (e.g., session termination).
Applications Where Simplicity is Prioritized: Great for smaller to medium-sized apps where ease of implementation and quick development are key, especially when using frameworks with built-in session support.
When to Consider Alternatives:
Microservices Architectures: In distributed systems, managing centralized sessions can be complex and inefficient. In that case, using JWTs is beneficial as they are better suited for microservices, offering stateless authentication and enabling independent service verification
Mobile Applications: While possible, session-based authentication isn’t ideal for mobile apps, which typically interact with APIs better suited to stateless methods like JWTs or API keys.
High-Scalability Requirements: For apps expecting a large number of concurrent users that require horizontal scaling, session state can become a bottleneck. Stateless authentication, such as JWTs, is a better alternative.
Stateless APIs: If you’re developing a stateless API with each request requiring its unique authentication information, session-based authentication, which relies on server-side state, isn’t a good option.
Specific Examples
Ideal Use Cases:
Banking Applications: Session-based authentication works well in online banking with strict security measures, including short session timeouts, HTTPS, secure cookies, and CSRF protection. Multi-factor authentication (MFA) is typically added for extra security.
Social Media Platforms: Social media often uses it due to features like “remember me” and tracking user activity. Large platforms often use distributed session stores or hybrid approaches to manage the massive scale.
E-commerce Sites: Well-suited for e-commerce, as it supports features like persistent shopping carts across pages and user sessions.
When to Avoid:
- API-Driven Applications: For frontends like single-page applications or mobile apps interacting with a backend API, JWTs or API keys are generally a better choice than session-based authentication for handling API requests.
Conclusion
Session-based authentication is a tried-and-true way to secure web applications. It’s simple, gives you server-side management, and lets you manage state more effectively. However, like with any technology, there is no one-size-fits-all answer.
Understanding its merits and limitations allows you to determine whether it’s the best fit for your app or if you should look into alternative choices such as JWTs, OAuth 2.0, API keys, or multi-factor authentication (MFA). For password less login, approaches such as magic links may be more appropriate.
In the next article of this series, we’ll look into JWTs, including when to use them, their benefits, and how to use them. Stay tuned for more on creating secure online applications!
Thank you for reading! If you find this article helpful, feel free to highlight, clap, leave a comment, or even reach out to me on Twitter/X and LinkedIn as it’s very appreciated and helps keeps content coming!
Subscribe to my newsletter
Read articles from Manash Jyoti Baruah directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Manash Jyoti Baruah
Manash Jyoti Baruah
Software Engineer ~ Challenging myself to grow ~ Join me in the journey of self-improvement and building solutions that inspire change.