← Back to DevBytes

ESBuild Authentication: JWT, Sessions, and OAuth Integration

ESBuild and Authentication: A Modern Integration Guide

ESBuild has become the go-to bundler for many JavaScript and TypeScript projects because of its extreme speed and minimal configuration. Yet a bundler alone cannot secure your application. Authentication — whether through JSON Web Tokens (JWT), traditional sessions, or OAuth — remains a cornerstone of modern web development. This tutorial shows you how to weave authentication into an application whose build pipeline is powered by ESBuild. We’ll cover practical implementations for both Node.js backends and browser frontends, all bundled with ESBuild.

What Is “ESBuild Authentication”?

The phrase “ESBuild authentication” doesn’t refer to a built-in feature of ESBuild. Instead, it describes the practice of building authentication logic (JWT verification, session middleware, OAuth handlers) inside projects that use ESBuild for their compilation and bundling. Because ESBuild can target Node.js, browser, and even serverless environments, understanding how to bundle authentication code efficiently and securely is essential for modern full‑stack workflows.

Why Authentication Matters in ESBuild-Powered Apps

Authentication controls who can access your APIs and pages. Even the fastest bundler won’t protect you if an unauthenticated user reaches protected resources. Integrating authentication correctly ensures:

Setting Up Your ESBuild Project

Before diving into authentication, we need a basic project structure. Assume we have a monorepo with a server folder (Node.js API) and a client folder (browser app). We’ll use a single build.mjs script to orchestrate ESBuild.

// build.mjs
import * as esbuild from 'esbuild';

// Bundle server (Node.js target, external dependencies)
await esbuild.build({
  entryPoints: ['server/src/index.ts'],
  bundle: true,
  platform: 'node',
  outfile: 'server/dist/bundle.js',
  external: ['express', 'jsonwebtoken', 'express-session', 'passport', 'cookie-parser'],
});

// Bundle client (browser target)
await esbuild.build({
  entryPoints: ['client/src/main.ts'],
  bundle: true,
  platform: 'browser',
  outfile: 'client/dist/bundle.js',
  define: { 'process.env.API_URL': '"http://localhost:3000"' },
});

This script bundles server and client separately. We keep authentication libraries external on the server so they aren’t inlined; ESBuild will simply require them at runtime.

Implementing JWT Authentication

Backend: Generating and Verifying JWTs

We’ll create a simple Express server that issues JWTs on login and protects routes with a verification middleware.

// server/src/auth/jwt.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

const SECRET = process.env.JWT_SECRET || 'change-me-in-production';

export interface JwtPayload {
  userId: string;
  role: string;
}

export function generateToken(payload: JwtPayload): string {
  return jwt.sign(payload, SECRET, { expiresIn: '1h' });
}

export function verifyToken(token: string): JwtPayload {
  return jwt.verify(token, SECRET) as JwtPayload;
}

// Express middleware
export function authenticateJwt(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed token' });
  }
  const token = authHeader.split(' ')[1];
  try {
    const payload = verifyToken(token);
    (req as any).user = payload;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}
// server/src/index.ts (partial)
import express from 'express';
import { generateToken, authenticateJwt } from './auth/jwt';

const app = express();
app.use(express.json());

app.post('/login', (req, res) => {
  // Validate credentials (placeholder)
  const { username, password } = req.body;
  if (username === 'admin' && password === 'secret') {
    const token = generateToken({ userId: '1', role: 'admin' });
    return res.json({ token });
  }
  res.status(401).json({ error: 'Invalid credentials' });
});

app.get('/me', authenticateJwt, (req, res) => {
  res.json({ user: (req as any).user });
});

app.listen(3000);

After bundling with node build.mjs, the server runs as node server/dist/bundle.js. ESBuild resolves all local imports, leaving only express and jsonwebtoken as external.

Client-Side JWT Handling

The browser client stores the JWT (preferably in memory or a secure httpOnly cookie, but for demonstration we’ll use localStorage) and attaches it to fetch requests.

// client/src/auth.ts
const API = process.env.API_URL;

export async function login(username: string, password: string): Promise {
  const res = await fetch(`${API}/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  });
  if (!res.ok) return null;
  const data = await res.json();
  localStorage.setItem('token', data.token);
  return data.token;
}

export async function fetchWithAuth(url: string, options: RequestInit = {}) {
  const token = localStorage.getItem('token');
  const headers = new Headers(options.headers || {});
  if (token) headers.set('Authorization', `Bearer ${token}`);
  return fetch(`${API}${url}`, { ...options, headers });
}

// Example usage in main.ts
document.getElementById('login-btn')?.addEventListener('click', async () => {
  const token = await login('admin', 'secret');
  if (token) {
    const res = await fetchWithAuth('/me');
    console.log(await res.json());
  }
});

The client bundle is a single file that runs in the browser, making authenticated API calls.

Session-Based Authentication

Backend: Express Sessions and Cookie Management

Session authentication relies on a server‑side store and a session cookie. We’ll use express-session with an in‑memory store (suitable for development).

// server/src/auth/session.ts
import session from 'express-session';
import { Request, Response, NextFunction } from 'express';

const SESSION_SECRET = process.env.SESSION_SECRET || 'session-secret-change-me';

export const sessionMiddleware = session({
  secret: SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 3600000, // 1 hour
  },
});

export function authenticateSession(req: Request, res: Response, next: NextFunction) {
  if ((req.session as any)?.userId) {
    next();
  } else {
    res.status(401).json({ error: 'Not authenticated' });
  }
}
// server/src/index.ts (session variant)
import express from 'express';
import cookieParser from 'cookie-parser';
import { sessionMiddleware, authenticateSession } from './auth/session';

const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(sessionMiddleware);

app.post('/login-session', (req, res) => {
  const { username, password } = req.body;
  if (username === 'admin' && password === 'secret') {
    (req.session as any).userId = '1';
    (req.session as any).role = 'admin';
    return res.json({ message: 'Logged in' });
  }
  res.status(401).json({ error: 'Invalid credentials' });
});

app.get('/me-session', authenticateSession, (req, res) => {
  res.json({ userId: (req.session as any).userId, role: (req.session as any).role });
});

app.post('/logout-session', (req, res) => {
  req.session.destroy(() => {
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});

Because express-session is marked as external in the ESBuild config, it won’t be bundled. The session cookie (connect.sid) is handled automatically by the browser.

Client-Side Considerations for Sessions

For session‑based auth, the client doesn’t need to manually attach tokens. Cookies are sent automatically with every request. The client code can simply call /me-session with fetch (credentials: 'include' if cross‑origin). ESBuild bundles the client normally.

// client/src/sessionClient.ts
const API = process.env.API_URL;

export async function checkSession() {
  const res = await fetch(`${API}/me-session`, { credentials: 'include' });
  return res.ok ? await res.json() : null;
}

OAuth Integration (Google OAuth Example)

Backend: Passport.js Strategy

OAuth allows users to log in with third‑party providers. We’ll implement Google OAuth2 using passport and passport-google-oauth20.

// server/src/auth/oauth.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { Request, Response, NextFunction } from 'express';

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID!;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET!;
const CALLBACK_URL = process.env.CALLBACK_URL || 'http://localhost:3000/auth/google/callback';

passport.use(new GoogleStrategy({
  clientID: GOOGLE_CLIENT_ID,
  clientSecret: GOOGLE_CLIENT_SECRET,
  callbackURL: CALLBACK_URL,
}, (accessToken, refreshToken, profile, done) => {
  // Find or create user based on profile.id
  const user = { id: profile.id, displayName: profile.displayName };
  return done(null, user);
}));

passport.serializeUser((user: any, done) => {
  done(null, user.id);
});

passport.deserializeUser((id: string, done) => {
  // Fetch user from database (placeholder)
  done(null, { id, displayName: 'Google User' });
});

export function ensureAuthenticated(req: Request, res: Response, next: NextFunction) {
  if (req.isAuthenticated()) return next();
  res.status(401).json({ error: 'Not authenticated' });
}
// server/src/index.ts (OAuth routes)
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import { ensureAuthenticated } from './auth/oauth';

const app = express();
app.use(session({ secret: 'oauth-secret', resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());

app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard'); // or send token/JSON
  }
);

app.get('/me-oauth', ensureAuthenticated, (req, res) => {
  res.json({ user: req.user });
});

When bundling with ESBuild, keep passport, passport-google-oauth20, and express-session external. Environment variables for OAuth secrets should never be bundled into the code; define them at runtime.

Frontend: Initiating OAuth Flow

The client typically redirects the user to /auth/google. After the callback, the server can issue a JWT or establish a session. The client then behaves like a standard authenticated app.

// client/src/oauthLogin.ts
const API = process.env.API_URL;

export function loginWithGoogle() {
  window.location.href = `${API}/auth/google`;
}

// On the callback page (e.g., dashboard.html) we check the session
export async function fetchOAuthUser() {
  const res = await fetch(`${API}/me-oauth`, { credentials: 'include' });
  return res.ok ? await res.json() : null;
}

The frontend bundle includes this logic, compiled by ESBuild alongside your main application code.

Best Practices for Authentication in ESBuild Projects

Conclusion

Integrating JWT, session, and OAuth authentication into an ESBuild‑based project is a matter of structuring your code cleanly and configuring the bundler appropriately. ESBuild’s speed and simplicity make it an excellent companion for modern authentication workflows. By externalising sensitive packages, keeping secrets in environment variables, and bundling client and server code separately, you can build secure, performant applications that protect user data without sacrificing development velocity. Whether you choose stateless JWTs, traditional sessions, or social OAuth, the principles remain the same: design your auth layer as a set of focused modules, let ESBuild handle the rest, and always prioritise security over convenience.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles