← Back to DevBytes

Webpack Authentication: JWT, Sessions, and OAuth Integration

Introduction to Webpack Authentication

Authentication is a critical component of any modern web application. When building applications bundled with Webpack, integrating authentication mechanisms like JWT (JSON Web Tokens), session-based authentication, and OAuth requires careful consideration of how your build pipeline handles tokens, redirects, and secure communication. This tutorial walks you through setting up a Webpack-based project with robust authentication using all three approaches.

What Is Webpack Authentication?

Webpack authentication refers to the strategies and configurations used to handle user identity verification in applications bundled by Webpack. Since Webpack is a module bundler rather than an authentication framework, "Webpack authentication" really means integrating authentication libraries and flows into a Webpack-powered frontend (and its backend API). This involves managing tokens in bundled JavaScript, configuring environment variables securely, handling redirects, and ensuring that sensitive data is not exposed in the build output.

Why It Matters

Without proper authentication integration, your application is vulnerable to unauthorized access, token leakage, and broken user sessions. A well-structured authentication setup ensures that:

Project Setup and Webpack Configuration

Before diving into authentication strategies, let's set up a basic Webpack project. We'll use Webpack 5 with Babel for modern JavaScript support.

First, initialize the project and install dependencies:

mkdir webpack-auth-demo
cd webpack-auth-demo
npm init -y
npm install webpack webpack-cli webpack-dev-server \
  html-webpack-plugin babel-loader @babel/core @babel/preset-env \
  axios dotenv --save-dev

Next, create the Webpack configuration file. This configuration includes the DefinePlugin to inject environment variables safely and the HtmlWebpackPlugin to generate the HTML entry point:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
require('dotenv').config();

module.exports = {
  mode: process.env.NODE_ENV || 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.[contenthash].js',
    clean: true,
  },
  devServer: {
    static: './dist',
    hot: true,
    historyApiFallback: true,
    port: 3000,
    proxy: {
      '/api': 'http://localhost:5000',
    },
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL || 'http://localhost:5000'),
      'process.env.OAUTH_CLIENT_ID': JSON.stringify(process.env.OAUTH_CLIENT_ID || ''),
    }),
  ],
};

Create a .env file for local development. Never commit this file to version control:

# .env
API_URL=http://localhost:5000
OAUTH_CLIENT_ID=your_client_id_here
NODE_ENV=development

JWT Authentication

JSON Web Tokens are a stateless authentication mechanism. The server issues a signed token after login, and the client includes it in subsequent requests. JWTs are popular in single-page applications because they scale well and work naturally with APIs.

How JWT Works

A JWT consists of three parts: a header, a payload, and a signature. The header describes the token type and signing algorithm. The payload contains claims such as user ID and expiration. The signature ensures the token has not been tampered with. The client typically stores the JWT in memory or localStorage and sends it in the Authorization header as a Bearer token.

Implementing JWT on the Client Side

Create an authentication service module that handles login, token storage, and API requests. We'll store the access token in memory and a refresh token in an HTTP-only cookie for added security:

// src/auth/jwtAuthService.js
import axios from 'axios';

const API_URL = process.env.API_URL;

let accessToken = null;

const api = axios.create({
  baseURL: API_URL,
  withCredentials: true,
});

// Request interceptor: attach token automatically
api.interceptors.request.use((config) => {
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// Response interceptor: handle token refresh on 401
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        const res = await axios.post(`${API_URL}/auth/refresh`, {}, {
          withCredentials: true,
        });
        accessToken = res.data.accessToken;
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return api(originalRequest);
      } catch (refreshError) {
        accessToken = null;
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

export const jwtAuth = {
  async login(email, password) {
    const res = await api.post('/auth/login', { email, password });
    accessToken = res.data.accessToken;
    return res.data.user;
  },

  async register(email, password) {
    const res = await api.post('/auth/register', { email, password });
    accessToken = res.data.accessToken;
    return res.data.user;
  },

  async getProfile() {
    const res = await api.get('/auth/me');
    return res.data.user;
  },

  async logout() {
    await api.post('/auth/logout', {}, { withCredentials: true });
    accessToken = null;
  },

  isAuthenticated() {
    return accessToken !== null;
  },

  getToken() {
    return accessToken;
  },
};

Backend JWT Example

Here is a minimal Express server that issues and verifies JWTs. The access token is short-lived, while the refresh token is stored in an HTTP-only cookie:

// server/jwtServer.js
const express = require('express');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const bcrypt = require('bcryptjs');

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

const ACCESS_SECRET = process.env.ACCESS_SECRET || 'access_secret_dev';
const REFRESH_SECRET = process.env.REFRESH_SECRET || 'refresh_secret_dev';

// Mock user store
const users = [];

function generateAccessToken(userId) {
  return jwt.sign({ userId }, ACCESS_SECRET, { expiresIn: '15m' });
}

function generateRefreshToken(userId) {
  return jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
}

app.post('/auth/register', async (req, res) => {
  const { email, password } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  const user = { id: Date.now().toString(), email, password: hashed };
  users.push(user);
  const accessToken = generateAccessToken(user.id);
  const refreshToken = generateRefreshToken(user.id);
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.json({ accessToken, user: { id: user.id, email: user.email } });
});

app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users.find((u) => u.email === email);
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const accessToken = generateAccessToken(user.id);
  const refreshToken = generateRefreshToken(user.id);
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.json({ accessToken, user: { id: user.id, email: user.email } });
});

app.post('/auth/refresh', (req, res) => {
  const token = req.cookies.refreshToken;
  if (!token) return res.status(401).json({ error: 'No refresh token' });
  try {
    const payload = jwt.verify(token, REFRESH_SECRET);
    const accessToken = generateAccessToken(payload.userId);
    res.json({ accessToken });
  } catch (err) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

app.get('/auth/me', (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(401).json({ error: 'No token' });
  const token = authHeader.split(' ')[1];
  try {
    const payload = jwt.verify(token, ACCESS_SECRET);
    const user = users.find((u) => u.id === payload.userId);
    res.json({ user: { id: user.id, email: user.email } });
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

app.post('/auth/logout', (req, res) => {
  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out' });
});

app.listen(5000, () => console.log('JWT server running on port 5000'));

Session-Based Authentication

Session-based authentication is a stateful approach where the server stores session data and sends a session ID to the client via a cookie. The client automatically includes the cookie in subsequent requests. This approach is simpler than JWT in some ways because the server controls the session lifecycle entirely.

When to Use Sessions

Client-Side Session Handling

With session-based auth, the client does not need to manage tokens manually. The browser handles cookies automatically. However, you must configure Axios to send credentials:

// src/auth/sessionAuthService.js
import axios from 'axios';

const API_URL = process.env.API_URL;

const api = axios.create({
  baseURL: API_URL,
  withCredentials: true,
});

export const sessionAuth = {
  async login(email, password) {
    const res = await api.post('/session/login', { email, password });
    return res.data.user;
  },

  async logout() {
    await api.post('/session/logout');
  },

  async getProfile() {
    const res = await api.get('/session/me');
    return res.data.user;
  },

  async checkSession() {
    try {
      const res = await api.get('/session/me');
      return res.data.user;
    } catch {
      return null;
    }
  },
};

Backend Session Server

Here is an Express server using express-session with a memory store. In production, replace the memory store with Redis or another persistent store:

// server/sessionServer.js
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');

const app = express();
app.use(express.json());
app.use(
  session({
    secret: process.env.SESSION_SECRET || 'session_secret_dev',
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 24 * 60 * 60 * 1000,
    },
  })
);

const users = [];

app.post('/session/register', async (req, res) => {
  const { email, password } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  const user = { id: Date.now().toString(), email, password: hashed };
  users.push(user);
  req.session.userId = user.id;
  res.json({ user: { id: user.id, email: user.email } });
});

app.post('/session/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users.find((u) => u.email === email);
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  req.session.userId = user.id;
  res.json({ user: { id: user.id, email: user.email } });
});

app.get('/session/me', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  const user = users.find((u) => u.id === req.session.userId);
  if (!user) return res.status(401).json({ error: 'User not found' });
  res.json({ user: { id: user.id, email: user.email } });
});

app.post('/session/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});

app.listen(5000, () => console.log('Session server running on port 5000'));

OAuth Integration

OAuth allows users to authenticate using third-party providers like Google, GitHub, or Facebook. Instead of managing passwords yourself, you redirect users to the provider, and after they authorize your application, the provider sends back an authorization code that your server exchanges for an access token.

OAuth Flow Overview

The OAuth 2.0 authorization code flow works as follows:

Client-Side OAuth Initiation

Create a module that handles the OAuth redirect. The client simply navigates to the provider's authorization URL:

// src/auth/oauthService.js
const API_URL = process.env.API_URL;
const GOOGLE_CLIENT_ID = process.env.OAUTH_CLIENT_ID;

export const oauthAuth = {
  loginWithGoogle() {
    const redirectUri = encodeURIComponent(`${window.location.origin}/auth/google/callback`);
    const scope = encodeURIComponent('openid email profile');
    const state = this.generateState();
    sessionStorage.setItem('oauth_state', state);

    const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
      `client_id=${GOOGLE_CLIENT_ID}&` +
      `redirect_uri=${redirectUri}&` +
      `response_type=code&` +
      `scope=${scope}&` +
      `state=${state}`;

    window.location.href = authUrl;
  },

  generateState() {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('');
  },

  async handleCallback(code, state) {
    const savedState = sessionStorage.getItem('oauth_state');
    if (state !== savedState) {
      throw new Error('Invalid OAuth state');
    }
    sessionStorage.removeItem('oauth_state');

    const res = await fetch(`${API_URL}/auth/google`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ code }),
    });

    if (!res.ok) throw new Error('OAuth authentication failed');
    return res.json();
  },
};

Handling the OAuth Callback in Webpack

Since Webpack dev server supports historyApiFallback, you can create a dedicated callback route in your SPA. Create a callback handler module:

// src/auth/oauthCallback.js
import { oauthAuth } from './oauthService';

export async function handleOAuthCallback() {
  const url = new URL(window.location.href);
  const code = url.searchParams.get('code');
  const state = url.searchParams.get('state');
  const error = url.searchParams.get('error');

  if (error) {
    console.error('OAuth error:', error);
    window.location.href = '/login?error=oauth_denied';
    return;
  }

  if (!code || !state) {
    window.location.href = '/login';
    return;
  }

  try {
    const result = await oauthAuth.handleCallback(code, state);
    console.log('OAuth login successful:', result.user);
    window.location.href = '/dashboard';
  } catch (err) {
    console.error('OAuth callback failed:', err);
    window.location.href = '/login?error=oauth_failed';
  }
}

Backend OAuth Handler

Here is an Express route that exchanges the authorization code for a token and creates a JWT for your application:

// server/oauthServer.js
const express = require('express');
const jwt = require('jsonwebtoken');
const axios = require('axios');
const cookieParser = require('cookie-parser');

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

const ACCESS_SECRET = process.env.ACCESS_SECRET || 'access_secret_dev';
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;

const users = new Map();

app.post('/auth/google', async (req, res) => {
  const { code } = req.body;

  try {
    // Exchange code for tokens
    const tokenRes = await axios.post('https://oauth2.googleapis.com/token', {
      code,
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      redirect_uri: `${req.headers.origin}/auth/google/callback`,
      grant_type: 'authorization_code',
    });

    const { access_token, id_token } = tokenRes.data;

    // Fetch user profile
    const profileRes = await axios.get(
      'https://www.googleapis.com/oauth2/v2/userinfo',
      { headers: { Authorization: `Bearer ${access_token}` } }
    );

    const { id, email, name } = profileRes.data;

    // Find or create user
    let user = users.get(id);
    if (!user) {
      user = { id, email, name, provider: 'google' };
      users.set(id, user);
    }

    // Issue your own JWT
    const appToken = jwt.sign({ userId: user.id, provider: 'google' }, ACCESS_SECRET, {
      expiresIn: '15m',
    });

    const refreshToken = jwt.sign({ userId: user.id }, process.env.REFRESH_SECRET || 'refresh_secret_dev', {
      expiresIn: '7d',
    });

    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000,
    });

    res.json({ accessToken: appToken, user });
  } catch (err) {
    console.error('OAuth exchange failed:', err.response?.data || err.message);
    res.status(400).json({ error: 'OAuth authentication failed' });
  }
});

app.listen(5000, () => console.log('OAuth server running on port 5000'));

Building a Unified Auth Context

To make authentication manageable across your application, create a unified auth context that can switch between JWT, session, and OAuth strategies. This pattern works well with vanilla JavaScript or can be adapted for frameworks like React:

// src/auth/authManager.js
import { jwtAuth } from './jwtAuthService';
import { sessionAuth } from './sessionAuthService';
import { oauthAuth } from './oauthService';

const AUTH_STRATEGY = process.env.AUTH_STRATEGY || 'jwt';

const strategies = {
  jwt: jwtAuth,
  session: sessionAuth,
};

export const authManager = {
  currentStrategy: AUTH_STRATEGY,

  async login(email, password) {
    if (this.currentStrategy === 'session') {
      return sessionAuth.login(email, password);
    }
    return jwtAuth.login(email, password);
  },

  async logout() {
    if (this.currentStrategy === 'session') {
      return sessionAuth.logout();
    }
    return jwtAuth.logout();
  },

  async getProfile() {
    if (this.currentStrategy === 'session') {
      return sessionAuth.getProfile();
    }
    return jwtAuth.getProfile();
  },

  loginWithProvider(provider) {
    if (provider === 'google') {
      oauthAuth.loginWithGoogle();
    }
  },

  isAuthenticated() {
    if (this.currentStrategy === 'jwt') {
      return jwtAuth.isAuthenticated();
    }
    return false; // Session auth requires async check
  },
};

Protecting Routes in a Webpack SPA

When building a single-page application with Webpack, you need client-side route protection. Here is a simple router guard that checks authentication before allowing access to protected views:

// src/router/routeGuard.js
import { authManager } from '../auth/authManager';

const routes = {
  '/': 'home',
  '/login': 'login',
  '/register': 'register',
  '/dashboard': 'dashboard',
  '/auth/google/callback': 'oauth-callback',
};

const protectedRoutes = ['/dashboard'];

export async function navigate(path) {
  if (protectedRoutes.includes(path)) {
    try {
      const user = await authManager.getProfile();
      if (!user) {
        window.location.href = '/login';
        return;
      }
    } catch {
      window.location.href = '/login';
      return;
    }
  }

  const view = routes[path] || 'not-found';
  renderView(view);
}

function renderView(view) {
  const app = document.getElementById('app');
  app.innerHTML = `<h1>${view}</h1>`;
}

window.addEventListener('popstate', () => {
  navigate(window.location.pathname);
});

Best Practices

Secure Token Storage

Avoid storing access tokens in localStorage if possible, as they are vulnerable to XSS attacks. The recommended approach is to keep access tokens in memory and refresh tokens in HTTP-only cookies. If you must use localStorage, implement Content Security Policy headers and sanitize all user input to reduce XSS risk.

Use HTTPS Everywhere

Always serve your application over HTTPS in production. Set the secure flag on cookies so they are only transmitted over encrypted connections. Configure your Webpack dev server proxy to forward to an HTTPS backend when appropriate.

Validate OAuth State Parameters

The state parameter in OAuth flows prevents CSRF attacks. Always generate a cryptographically random state value, store it before redirecting, and verify it when handling the callback. The examples above demonstrate this pattern.

Keep Secrets Out of Bundles

Never put API secrets, client secrets, or private keys in your frontend code. Webpack bundles are visible to anyone who inspects your application. Use the DefinePlugin only for public values like client IDs and API URLs. Keep server secrets in environment variables on the backend.

Implement Token Expiration

Use short-lived access tokens (15 minutes or less) and longer-lived refresh tokens. Implement automatic refresh logic in your Axios interceptors so users do not experience unexpected logouts. Always handle refresh token expiration gracefully by redirecting to login.

Enable CORS Correctly

Configure CORS on your backend to allow credentials from your frontend origin. For session-based auth and refresh token cookies, set Access-Control-Allow-Credentials: true and specify the exact origin rather than using a wildcard:

// Example CORS configuration
const cors = require('cors');
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
}));

Log Out Properly

Always call the logout endpoint on your server to invalidate sessions or refresh tokens. Simply clearing client-side storage is not enough because refresh tokens in cookies would remain valid until expiration.

Conclusion

Integrating authentication into a Webpack-based application involves more than just adding a login form. You need to consider how tokens flow through your build pipeline, how the frontend communicates with your backend, and how different authentication strategies fit your use case. JWT offers a stateless, scalable approach ideal for API-driven applications. Session-based authentication provides simplicity and instant revocation at the cost of server-side state. OAuth enables passwordless login through trusted providers, improving user experience and security. By combining these strategies with proper Webpack configuration, secure token handling, route protection, and adherence to best practices, you can build a robust authentication system that protects your users and scales with your application. Start with the approach that best fits your current needs, and remember that authentication is never a set-it-and-forget-it concern — review and update your security posture regularly as your application evolves.

— Ad —

Google AdSense will appear here after approval

← Back to all articles