Skip to Content
chalvien 1.0 is released
DocumentationGuidesSecurityAuthenticationJWTJWT Express Implementation

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-parser

Types for TypeScript:

npm install -D @types/jsonwebtoken @types/bcrypt

Environment 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=true

JWT 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

  1. User logs in
  2. Server verifies credentials
  3. Server issues JWT access token
  4. Server sets refresh token cookie
  5. Client sends JWT in Authorization header
  6. Middleware verifies token
  7. Protected route executes

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