Express Authentication Implementation
Express + Prisma + PostgreSQL
This document provides a practical implementation reference for authentication in the Express backend.
Stack:
- Express API
- Prisma ORM
- PostgreSQL
- JWT (HS256)
- bcrypt password hashing
Frontend (Next.js) acts as a pure HTTP consumer.
Required Dependencies
Install required packages:
npm install jsonwebtoken bcrypt cookie-parserTypes for TypeScript:
npm install -D @types/jsonwebtoken @types/bcryptEnvironment Variables
Example .env:
JWT_SECRET=your_secure_secret
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=7d
REFRESH_COOKIE_NAME=refresh_token
NODE_ENV=development
ALLOW_BOOTSTRAP=trueJWT Utility Functions
src/utils/jwt.ts
jwt.ts
const jwt = require("jsonwebtoken");
function generateAccessToken(user) {
return jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_ACCESS_TTL || "15m" }
);
}
function verifyToken(token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
module.exports = {
generateAccessToken,
verifyToken
};Login Controller
src/controllers/auth/login.js
login.js
const bcrypt = require("bcrypt");
const prisma = require("../../prisma");
const { generateAccessToken } = require("../../utils/jwt");
async function login(req, res) {
const { email, password } = req.body;
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: "Invalid credentials" });
}
const accessToken = generateAccessToken(user);
const refreshToken = generateAccessToken(user);
res.cookie("refresh_token", refreshToken, {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
path: "/api/auth"
});
res.json({
accessToken,
user: {
id: user.id,
email: user.email,
role: user.role
}
});
}
module.exports = login;Refresh Token Controller
src/controllers/auth/refresh.js
refreh.js
const { generateAccessToken, verifyToken } = require("../../utils/jwt");
function refresh(req, res) {
const token = req.cookies.refresh_token;
if (!token) {
return res.status(401).json({ error: "Missing refresh token" });
}
try {
const payload = verifyToken(token);
const newAccessToken = generateAccessToken({
id: payload.sub,
email: payload.email,
role: payload.role
});
res.json({
accessToken: newAccessToken
});
} catch (err) {
return res.status(401).json({ error: "Invalid refresh token" });
}
}
module.exports = refresh;Logout Controller
src/controllers/auth/logout.js
logout.js
function logout(req, res) {
res.clearCookie("refresh_token", {
path: "/api/auth"
});
res.json({
message: "Logged out successfully"
});
}
module.exports = logout;JWT Authentication Middleware
src/middleware/auth.js
auth.js
const { verifyToken } = require("../utils/jwt");
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: "Access token required" });
}
const token = authHeader.split(" ")[1];
try {
const payload = verifyToken(token);
req.user = {
id: payload.sub,
email: payload.email,
role: payload.role
};
next();
} catch (err) {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
module.exports = authenticate;Role Authorization Middleware
src/middleware/requireRole.js
requireRole.js
function requireRole(role) {
return function (req, res, next) {
if (!req.user) {
return res.status(401).json({ error: "Unauthorized" });
}
if (req.user.role !== role) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
module.exports = requireRole;Example usage:
router.get(
"/admin/users",
authenticate,
requireRole("ADMIN"),
controller
);Bootstrap Admin Controller
src/controllers/auth/bootstrap.js
bootstrap.js
const bcrypt = require("bcrypt");
const prisma = require("../../prisma");
async function bootstrap(req, res) {
if (
process.env.NODE_ENV === "production" &&
process.env.ALLOW_BOOTSTRAP !== "true"
) {
return res.status(403).json({
error: "Bootstrap disabled"
});
}
const existing = await prisma.user.count();
if (existing > 0) {
return res.status(409).json({
error: "Admin already exists"
});
}
const { email, password, name } = req.body;
const hash = await bcrypt.hash(password, 10);
await prisma.user.create({
data: {
email,
password: hash,
name,
role: "ADMIN"
}
});
res.json({
message: "Admin user created successfully"
});
}
module.exports = bootstrap;Express Route Setup
src/routes/auth.js
auth.js
const express = require("express");
const login = require("../controllers/auth/login");
const refresh = require("../controllers/auth/refresh");
const logout = require("../controllers/auth/logout");
const bootstrap = require("../controllers/auth/bootstrap");
const router = express.Router();
router.post("/login", login);
router.post("/refresh", refresh);
router.post("/logout", logout);
router.post("/bootstrap", bootstrap);
module.exports = router;Express App Configuration
src/app.js
app.js
const express = require("express");
const cookieParser = require("cookie-parser");
const authRoutes = require("./routes/auth");
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use("/api/auth", authRoutes);
module.exports = app;Example Protected Route
router.get(
"/orders",
authenticate,
async (req, res) => {
const orders = await prisma.order.findMany();
res.json(orders);
}
);
Authentication Lifecycle
- User logs in
- Server verifies credentials
- Server issues JWT access token
- Server sets refresh token cookie
- Client sends JWT in Authorization header
- Middleware verifies token
- Protected route executes
Recommended Security Improvements
For production systems consider:
- rotating refresh tokens
- storing refresh tokens in database
- token revocation lists
- login rate limiting
- audit logs
- IP/device tracking
- CSRF protection
Design Philosophy
The Express backend fully owns authentication.
The frontend:
- does not manage sessions
- does not store refresh tokens
- only sends HTTP requests
This architecture allows:
- multiple client types (web, mobile, CLI)
- centralized security
- independent scaling of frontend and backend