Express Authentication: JWT, Sessions, and OAuth Integration
Authentication is the backbone of any secure web application. In the Express.js ecosystem, three dominant patterns have emerged for verifying user identity: JWT (JSON Web Tokens), session-based authentication, and OAuth integration for third-party login. This tutorial walks you through each approach with complete, production-ready code examples, explaining not just how to implement them but when to choose one over the others.
What Is Authentication in Express?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Authentication is the process of confirming that a user is who they claim to be. In an Express application, this typically involves receiving credentials (username/password or a third-party token), validating them against a data store or external service, and then establishing a persistent identity that travels with subsequent requests. This persistent identity can take the form of a session cookie stored on the server, a cryptographically signed JWT held by the client, or an OAuth token obtained from a trusted provider like Google or GitHub.
The Three Pillars
- JWT (JSON Web Tokens) — Stateless, self-contained tokens that encode user information and are verified using a secret or public key
- Server-side Sessions — Stateful approach where user identity is stored on the server and a session ID is sent via a cookie
- OAuth 2.0 — Delegated authorization protocol allowing users to log in via third-party identity providers
Why Authentication Matters
Without proper authentication, your application has no reliable way to:
- Protect sensitive user data from unauthorized access
- Personalize experiences based on user identity
- Audit actions and maintain accountability
- Comply with legal frameworks like GDPR, HIPAA, or SOC 2
- Prevent abuse of rate-limited or paid resources
A poorly implemented authentication system is the single largest attack vector in web applications. Understanding the trade-offs between JWT, sessions, and OAuth lets you choose the right tool for your specific use case—whether you're building a single-page application, a mobile backend, a microservices architecture, or a traditional server-rendered app.
Project Setup
Before diving into each method, let's set up a common Express foundation. Create a new project and install the base dependencies:
mkdir express-auth-tutorial && cd express-auth-tutorial
npm init -y
npm install express dotenv bcryptjs
npm install --save-dev nodemon
Create the foundational server file app.js that we'll extend for each authentication method:
// app.js - Base Express Server
const express = require('express');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// Built-in middleware to parse JSON bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Dummy user database (replace with real DB in production)
const users = [
{
id: 1,
username: 'alice',
// Pre-hashed password for "password123" using bcrypt
passwordHash: '$2b$10$ExampleHashReplaceWithRealBcryptValue',
email: 'alice@example.com',
role: 'user'
}
];
// Public route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Express Auth Tutorial API' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
1. JWT Authentication (Stateless)
How JWT Works
JWT authentication is stateless: the server issues a signed token containing user claims (like user ID and role), and the client stores this token—typically in localStorage, sessionStorage, or an HTTP-only cookie. On every subsequent request, the client sends the token (usually in the Authorization header as a Bearer token), and the server verifies the signature without needing to query a session store. This makes JWT ideal for distributed systems, microservices, and mobile backends where server-side session storage becomes a bottleneck.
A JWT consists of three base64-encoded parts separated by dots: header.payload.signature. The signature ensures integrity—any tampering with the payload invalidates the token.
Install JWT Dependencies
npm install jsonwebtoken
Complete JWT Implementation
// jwt-auth.js - Complete JWT Authentication Module
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-key';
const ACCESS_TOKEN_EXPIRY = '15m'; // Short-lived access token
const REFRESH_TOKEN_EXPIRY = '7d'; // Longer-lived refresh token
// In-memory refresh token store (use Redis in production)
const refreshTokensStore = new Map();
// --- Helper: Generate Tokens ---
function generateAccessToken(user) {
const payload = {
sub: user.id, // Subject (user ID)
username: user.username,
role: user.role,
iat: Math.floor(Date.now() / 1000), // Issued at
type: 'access'
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
}
function generateRefreshToken(user) {
const payload = {
sub: user.id,
type: 'refresh',
jti: `${user.id}-${Date.now()}` // Unique token ID for revocation
};
const token = jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
// Store refresh token for potential revocation
refreshTokensStore.set(payload.jti, { userId: user.id, expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 });
return token;
}
// --- Login Endpoint ---
app.post('/auth/jwt/login', async (req, res) => {
const { username, password } = req.body;
// Find user (use database query in production)
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Compare password hash
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token pair
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.status(200).json({
message: 'Login successful',
accessToken,
refreshToken,
expiresIn: 900 // 15 minutes in seconds
});
});
// --- Refresh Token Endpoint ---
app.post('/auth/jwt/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
try {
const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
// Check if token exists in our store (not revoked)
const storedToken = refreshTokensStore.get(decoded.jti);
if (!storedToken) {
return res.status(403).json({ error: 'Refresh token has been revoked' });
}
// Check expiration
if (Date.now() > storedToken.expiresAt) {
refreshTokensStore.delete(decoded.jti);
return res.status(403).json({ error: 'Refresh token expired' });
}
// Find user (in production, fetch fresh user data from DB)
const user = users.find(u => u.id === decoded.sub);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Issue new access token
const newAccessToken = generateAccessToken(user);
res.status(200).json({
accessToken: newAccessToken,
expiresIn: 900
});
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(403).json({ error: 'Refresh token expired' });
}
return res.status(403).json({ error: 'Invalid refresh token' });
}
});
// --- Logout / Revoke Refresh Token ---
app.post('/auth/jwt/logout', (req, res) => {
const { refreshToken } = req.body;
if (refreshToken) {
try {
const decoded = jwt.decode(refreshToken);
if (decoded && decoded.jti) {
refreshTokensStore.delete(decoded.jti);
}
} catch (err) {
// Token was malformed; proceed with logout anyway
}
}
res.status(200).json({ message: 'Logged out successfully' });
});
// --- JWT Verification Middleware ---
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token missing or malformed' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
// Attach user info to request object for downstream handlers
req.user = {
id: decoded.sub,
username: decoded.username,
role: decoded.role
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Access token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ error: 'Invalid access token' });
}
}
// --- Role-Based Authorization Middleware ---
function authorizeRole(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// --- Protected Route Example ---
app.get('/api/jwt/profile', authenticateJWT, (req, res) => {
res.json({
message: 'Protected profile data',
user: req.user,
timestamp: new Date().toISOString()
});
});
// --- Admin-only Route Example ---
app.get('/api/jwt/admin', authenticateJWT, authorizeRole('admin'), (req, res) => {
res.json({
message: 'Admin dashboard data',
sensitiveInfo: 'Only visible to admins'
});
});
JWT Best Practices Summary
- Keep access tokens short-lived (15-30 minutes) and use refresh tokens for renewal
- Store refresh tokens securely—use HTTP-only cookies or a secure server-side store, never localStorage for refresh tokens
- Never store sensitive data in the JWT payload; it's only base64-encoded, not encrypted
- Use strong secrets—at least 256 bits of entropy, stored in environment variables
- Implement token revocation via a blacklist or refresh token store for logout functionality
- Validate all claims including
exp,nbf, andaud(audience) in production
2. Session-Based Authentication (Stateful)
How Sessions Work
Session-based authentication stores user identity on the server. When a user logs in, the server creates a session record (in memory, a database, or a cache like Redis) and sends a session ID to the client as a cookie. On subsequent requests, the browser automatically includes this cookie, and the server looks up the session to identify the user. This approach is stateful but offers advantages: sessions can be invalidated instantly server-side, and no sensitive data leaves the server.
Install Session Dependencies
npm install express-session connect-mongo
# Optional but recommended for production session storage
npm install connect-redis redis
Complete Session Implementation
// session-auth.js - Complete Session-Based Authentication
const session = require('express-session');
const bcrypt = require('bcryptjs');
// --- Session Configuration ---
const sessionConfig = {
secret: process.env.SESSION_SECRET || 'a-very-long-random-session-secret-change-me',
resave: false, // Don't save session if unmodified
saveUninitialized: false, // Don't create session until something is stored
name: 'app_session', // Custom cookie name (hides default 'connect.sid')
cookie: {
httpOnly: true, // Prevent client-side JS access (XSS mitigation)
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
// For production, use a persistent store:
// store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }),
// or: new RedisStore({ client: redisClient })
};
app.use(session(sessionConfig));
// --- Login Endpoint (Session) ---
app.post('/auth/session/login', async (req, res) => {
const { username, password } = req.body;
// Validate input
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
// Find user
const user = users.find(u => u.username === username);
if (!user) {
// Use generic error to avoid user enumeration
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Regenerate session to prevent session fixation
req.session.regenerate((err) => {
if (err) {
console.error('Session regeneration error:', err);
return res.status(500).json({ error: 'Login failed' });
}
// Store user data in session (keep it minimal)
req.session.user = {
id: user.id,
username: user.username,
role: user.role,
loggedInAt: new Date().toISOString()
};
res.status(200).json({
message: 'Login successful',
user: {
id: user.id,
username: user.username,
role: user.role
}
});
});
});
// --- Logout Endpoint (Session) ---
app.post('/auth/session/logout', (req, res) => {
// Destroy the session completely
req.session.destroy((err) => {
if (err) {
console.error('Session destruction error:', err);
return res.status(500).json({ error: 'Logout failed' });
}
// Clear the session cookie
res.clearCookie('app_session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax'
});
res.status(200).json({ message: 'Logged out successfully' });
});
});
// --- Session Authentication Middleware ---
function authenticateSession(req, res, next) {
if (req.session && req.session.user) {
// Attach user to request for consistency with JWT pattern
req.user = req.session.user;
return next();
}
res.status(401).json({
error: 'Authentication required',
loginUrl: '/auth/session/login'
});
}
// --- Session Authorization Middleware ---
function authorizeSessionRole(...allowedRoles) {
return (req, res, next) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!allowedRoles.includes(req.session.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// --- Protected Route Example (Session) ---
app.get('/api/session/profile', authenticateSession, (req, res) => {
res.json({
message: 'Protected profile data via session',
user: req.session.user,
sessionId: req.sessionID,
timestamp: new Date().toISOString()
});
});
// --- Check Authentication Status ---
app.get('/auth/session/status', (req, res) => {
if (req.session && req.session.user) {
res.json({
authenticated: true,
user: req.session.user
});
} else {
res.json({ authenticated: false });
}
});
Session Best Practices Summary
- Use a production-grade session store like Redis or MongoDB—never rely on the default in-memory store in production (it leaks memory and doesn't scale across processes)
- Set cookie flags aggressively:
httpOnly,secure, andsameSiteprevent XSS and CSRF attacks - Regenerate session IDs on login and role changes to prevent session fixation
- Keep session payloads small—store only user ID and essential metadata, fetch full user data from the database when needed
- Set reasonable expiration and implement idle timeout logic for sensitive applications
3. OAuth 2.0 Integration (Third-Party Login)
How OAuth Works
OAuth 2.0 allows users to authenticate via a trusted third-party provider (like Google, GitHub, or Facebook) without sharing their credentials with your application. The flow works like this: your app redirects the user to the provider's authorization page, the user grants consent, the provider redirects back to your callback URL with an authorization code, your server exchanges that code for an access token, and finally uses that token to fetch the user's profile information from the provider's API.
Install OAuth Dependencies
npm install passport passport-google-oauth20
# For GitHub OAuth: npm install passport-github2
# For generic OAuth 2.0: npm install simple-oauth2
Complete OAuth Implementation (Google Example)
// oauth-auth.js - Complete OAuth 2.0 Integration with Google
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');
// --- OAuth Configuration ---
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const CALLBACK_URL = process.env.OAUTH_CALLBACK_URL || 'http://localhost:3000/auth/google/callback';
// Session middleware required for Passport's OAuth state management
app.use(session({
secret: process.env.SESSION_SECRET || 'oauth-session-secret',
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 3600000 }
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// --- In-memory user store for OAuth users (use DB in production) ---
const oauthUsers = new Map();
// --- Passport Serialization ---
// Serialize: store minimal user identifier in session
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Deserialize: fetch full user object when needed
passport.deserializeUser((id, done) => {
const user = oauthUsers.get(id);
if (user) {
done(null, user);
} else {
done(null, null);
}
});
// --- Google OAuth Strategy Configuration ---
passport.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: CALLBACK_URL,
// Request refresh token and user profile
scope: ['profile', 'email'],
// Force re-consent to get refresh token on each login
prompt: 'consent'
},
async (accessToken, refreshToken, profile, done) => {
try {
// Extract profile information
const googleId = profile.id;
const email = profile.emails && profile.emails[0] ? profile.emails[0].value : null;
const displayName = profile.displayName;
const avatar = profile.photos && profile.photos[0] ? profile.photos[0].value : null;
// Check if user exists in your database
let user = Array.from(oauthUsers.values()).find(u => u.googleId === googleId);
if (!user) {
// Create new user from OAuth profile
const newUserId = `oauth_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
user = {
id: newUserId,
googleId: googleId,
email: email,
displayName: displayName,
avatar: avatar,
accessToken: accessToken,
refreshToken: refreshToken || null,
provider: 'google',
role: 'user', // Default role for OAuth users
createdAt: new Date().toISOString()
};
oauthUsers.set(newUserId, user);
console.log(`New OAuth user created: ${displayName}`);
} else {
// Update tokens for existing user
user.accessToken = accessToken;
if (refreshToken) {
user.refreshToken = refreshToken;
}
console.log(`Returning OAuth user: ${user.displayName}`);
}
done(null, user);
} catch (error) {
console.error('OAuth strategy error:', error);
done(error, null);
}
}));
// --- OAuth Routes ---
// 1. Initiate OAuth flow: redirect user to Google's consent page
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email'],
prompt: 'select_account' // Let user choose which Google account
})
);
// 2. OAuth callback: Google redirects here after user consents
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/auth/oauth/error',
session: true
}),
(req, res) => {
// Successful authentication
console.log('OAuth login successful for:', req.user.displayName);
// Redirect to frontend with success indication
// In a real app, redirect to your SPA with a token or set a session
res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3001'}?login=success&provider=google`);
}
);
// 3. OAuth failure handler
app.get('/auth/oauth/error', (req, res) => {
res.status(401).json({
error: 'OAuth authentication failed',
message: 'User denied access or provider returned an error'
});
});
// --- OAuth Authenticated Middleware ---
function authenticateOAuth(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.status(401).json({
error: 'Authentication required',
loginUrl: '/auth/google'
});
}
// --- OAuth Protected Routes ---
app.get('/api/oauth/profile', authenticateOAuth, (req, res) => {
res.json({
message: 'OAuth-authenticated profile',
user: {
id: req.user.id,
displayName: req.user.displayName,
email: req.user.email,
avatar: req.user.avatar,
provider: req.user.provider,
role: req.user.role
}
});
});
// --- Logout for OAuth Sessions ---
app.post('/auth/oauth/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
req.session.destroy((destroyErr) => {
if (destroyErr) {
return res.status(500).json({ error: 'Session cleanup failed' });
}
res.clearCookie('app_session');
res.status(200).json({ message: 'Logged out successfully' });
});
});
});
// --- Token Refresh for OAuth (Google-specific) ---
app.post('/auth/oauth/refresh-token', authenticateOAuth, async (req, res) => {
// This endpoint refreshes the Google access token if expired
const user = req.user;
if (!user.refreshToken) {
return res.status(400).json({ error: 'No refresh token available' });
}
try {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
refresh_token: user.refreshToken,
grant_type: 'refresh_token'
})
});
const data = await response.json();
if (data.access_token) {
user.accessToken = data.access_token;
if (data.refresh_token) {
user.refreshToken = data.refresh_token;
}
res.json({ message: 'Token refreshed successfully' });
} else {
res.status(400).json({ error: 'Token refresh failed', details: data });
}
} catch (error) {
console.error('Token refresh error:', error);
res.status(500).json({ error: 'Token refresh request failed' });
}
});
OAuth Best Practices Summary
- Always validate the state parameter to prevent CSRF attacks in the OAuth flow (Passport handles this automatically)
- Store refresh tokens securely—encrypt them at rest in your database
- Verify the audience (aud) claim in ID tokens to ensure they were issued for your application
- Use the authorization code flow with PKCE for mobile and single-page applications (prevents authorization code interception)
- Scope access minimally—request only the scopes your application genuinely needs
- Handle token revocation—provide a way to revoke OAuth access from your application
Choosing the Right Authentication Method
Here's a practical decision framework based on real-world scenarios:
| Scenario | Recommended Method | Reason |
|---|---|---|
| SPA + REST API | JWT with refresh tokens | Stateless, works well with CORS, no CSRF concerns with Bearer tokens |
| Server-rendered MVC app | Server-side sessions | Natural fit with cookies, instant invalidation, no token management on client |
| Microservices | JWT (signed by auth service) | Each service can verify independently without calling auth service |
| Mobile app backend | JWT with long refresh | Mobile clients handle token storage well; sessions are problematic on mobile |
| Social login required | OAuth 2.0 + JWT or Sessions | OAuth handles third-party trust; combine with JWT/sessions for your own API |
| Real-time WebSocket app | JWT (passed in handshake) | WebSocket upgrades don't carry cookies; JWT can be sent as query param or header |
Hybrid Approach: Combining Methods
In production applications, you often combine approaches. For example, use OAuth for initial login and then issue a JWT for subsequent API calls. Here's a concise hybrid implementation:
// hybrid-auth.js - OAuth Login + JWT for API Access
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/auth/oauth/error' }),
(req, res) => {
// After successful OAuth login, generate JWT for API access
const accessToken = jwt.sign(
{
sub: req.user.id,
provider: req.user.provider,
role: req.user.role
},
JWT_SECRET,
{ expiresIn: '1h' }
);
// Return JWT to client (or set as HTTP-only cookie)
res.json({
message: 'OAuth login successful',
accessToken,
user: req.user
});
}
);
// Unified authentication middleware that accepts both JWT and session
function unifiedAuth(req, res, next) {
// Check session first
if (req.session && req.session.user) {
req.user = req.session.user;
return next();
}
// Fall back to JWT
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
try {
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, JWT_SECRET);
req.user = { id: decoded.sub, role: decoded.role };
return next();
} catch (err) {
// JWT invalid, fall through to 401
}
}
res.status(401).json({ error: 'Authentication required' });
}
Security Hardening: Beyond Basic Authentication
Rate Limiting Login Endpoints
// Install: npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window per IP
message: { error: 'Too many login attempts, please try again after 15 minutes' },
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true // Don't count successful logins
});
// Apply to all login endpoints
app.use('/auth/jwt/login', loginLimiter);
app.use('/auth/session/login', loginLimiter);
Helmet for Security Headers
// Install: npm install helmet
const helmet = require('helmet');
app.use(helmet()); // Adds CSP, HSTS, X-Frame-Options, and more
CSRF Protection for Session-Based Auth
// Install: npm install csurf
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// Provide CSRF token to client
app.get('/auth/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Apply CSRF protection to state-changing routes
app.post('/api/session/update-profile', authenticateSession, csrfProtection, (req, res) => {
// Profile update logic here
res.json({ message: 'Profile updated' });
});
Complete Environment Variables Template
Create a .env file with all the secrets and configuration values referenced throughout this tutorial:
# .env - Environment Variables for Express Auth Tutorial
# Server
PORT=3000
NODE_ENV=development
# JWT
JWT_SECRET=a-256-bit-or-longer-random-string-replace-with-real-secret
JWT_REFRESH_SECRET=another-very-long-random-string-different-from-above
# Sessions
SESSION_SECRET=a-third-long-random-string-for-sessions
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Frontend URL (for OAuth redirects)
FRONTEND_URL=http://localhost:3001
# Optional: Redis for session store
# REDIS_URL=redis://localhost:6379
# Optional: MongoDB for session store
# MONGO_URI=mongodb://localhost:27017/express-auth
Testing the Authentication Endpoints
Here's a quick curl-based test sequence for each method. First, hash a test password using bcrypt:
// hash-password.js - Utility to generate bcrypt hash for test users
const bcrypt = require('bcryptjs');
async function hashPassword(plainPassword) {
const salt = await bcrypt.genSalt(12);
const hash = await bcrypt.hash(plainPassword, salt);
console.log('Hashed password:', hash);
// Verify it works
const match = await bcrypt.compare(plainPassword, hash);
console.log('Verification:', match);
}