Node.js Integration Guide

Node.js Integration Guide

This guide shows you how to integrate Keycloak authentication into your Node.js applications. We’ll cover Express, Fastify, and vanilla Node.js implementations with production-ready examples.

⚠️
Important: The keycloak-connect npm package has been officially deprecated by Keycloak. This guide uses the recommended alternatives: openid-client for OAuth flows and jwks-rsa with jsonwebtoken for JWT validation.

Prerequisites

  • Node.js 18+ installed
  • A Keycloak instance (Skycloak account)
  • Basic understanding of OAuth 2.0/OpenID Connect
  • Your Keycloak realm and client configured

Quick Start

Installation

# Using npm - Recommended packages
npm install openid-client express-session jsonwebtoken jwks-rsa axios

# For Passport.js integration
npm install passport passport-openidconnect

# Using yarn
yarn add openid-client express-session jsonwebtoken jwks-rsa axios

# For TypeScript projects
npm install --save-dev @types/express @types/express-session @types/jsonwebtoken

Basic Express Setup with openid-client

const express = require('express');
const session = require('express-session');
const { Issuer, generators } = require('openid-client');

const app = express();

// Session configuration
app.use(session({
  secret: process.env.SESSION_SECRET || 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

let keycloakClient;

// Initialize OpenID Client
async function initializeOIDC() {
  const keycloakIssuer = await Issuer.discover(
    `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`
  );

  keycloakClient = new keycloakIssuer.Client({
    client_id: process.env.KEYCLOAK_CLIENT_ID,
    client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
    redirect_uris: [`${process.env.APP_URL}/auth/callback`],
    response_types: ['code'],
  });
}

// Auth middleware
const requireAuth = (req, res, next) => {
  if (!req.session.user) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  next();
};

// Login route
app.get('/auth/login', (req, res) => {
  const codeVerifier = generators.codeVerifier();
  const codeChallenge = generators.codeChallenge(codeVerifier);
  const state = generators.state();

  req.session.codeVerifier = codeVerifier;
  req.session.state = state;

  const authUrl = keycloakClient.authorizationUrl({
    scope: 'openid email profile',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state: state,
  });

  res.redirect(authUrl);
});

// Callback route
app.get('/auth/callback', async (req, res) => {
  try {
    const params = keycloakClient.callbackParams(req);
    const tokenSet = await keycloakClient.callback(
      `${process.env.APP_URL}/auth/callback`,
      params,
      {
        code_verifier: req.session.codeVerifier,
        state: req.session.state,
      }
    );

    const userInfo = await keycloakClient.userinfo(tokenSet.access_token);

    req.session.user = userInfo;
    req.session.tokens = {
      access_token: tokenSet.access_token,
      refresh_token: tokenSet.refresh_token,
      id_token: tokenSet.id_token,
    };

    delete req.session.codeVerifier;
    delete req.session.state;

    res.redirect('/dashboard');
  } catch (error) {
    console.error('Auth callback error:', error);
    res.status(500).json({ error: 'Authentication failed' });
  }
});

// Logout route
app.get('/auth/logout', async (req, res) => {
  const idToken = req.session.tokens?.id_token;
  req.session.destroy();

  if (idToken) {
    const logoutUrl = keycloakClient.endSessionUrl({ id_token_hint: idToken });
    res.redirect(logoutUrl);
  } else {
    res.redirect('/');
  }
});

// Protected route
app.get('/api/protected', requireAuth, (req, res) => {
  res.json({
    message: 'This is protected',
    user: req.session.user
  });
});

// Public route
app.get('/api/public', (req, res) => {
  res.json({ message: 'This is public' });
});

// Start server
initializeOIDC().then(() => {
  app.listen(3000, () => {
    console.log('Server running on port 3000');
  });
});

Passport.js Integration

For applications already using Passport.js:

const express = require('express');
const session = require('express-session');
const passport = require('passport');
const OpenIDConnectStrategy = require('passport-openidconnect');

const app = express();

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
}));

app.use(passport.initialize());
app.use(passport.session());

// Configure Passport with OpenID Connect
passport.use('keycloak', new OpenIDConnectStrategy({
  issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
  authorizationURL: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
  tokenURL: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
  userInfoURL: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
  clientID: process.env.KEYCLOAK_CLIENT_ID,
  clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
  callbackURL: `${process.env.APP_URL}/auth/callback`,
  scope: 'openid email profile',
}, (issuer, profile, context, idToken, accessToken, refreshToken, done) => {
  return done(null, {
    id: profile.id,
    email: profile.emails?.[0]?.value,
    name: profile.displayName,
    accessToken,
    refreshToken,
    idToken,
  });
}));

passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

// Routes
app.get('/auth/login', passport.authenticate('keycloak'));

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

app.get('/api/protected',
  (req, res, next) => req.isAuthenticated() ? next() : res.status(401).json({ error: 'Unauthorized' }),
  (req, res) => res.json({ user: req.user })
);

JWT Token Validation (API Protection)

For more control over token validation:

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const { promisify } = require('util');

// JWKS client setup
const client = jwksClient({
  jwksUri: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`,
  cache: true,
  cacheMaxEntries: 5,
  cacheMaxAge: 600000, // 10 minutes
  rateLimit: true,
  jwksRequestsPerMinute: 10
});

// Get signing key
const getKey = (header, callback) => {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) {
      return callback(err);
    }
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
};

// Promisify jwt.verify
const verifyToken = promisify(jwt.verify);

// Token validation middleware
const validateToken = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ error: 'No authorization header' });
  }
  
  const token = authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    // Verify token
    const decoded = await verifyToken(token, getKey, {
      algorithms: ['RS256'],
      issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
      audience: process.env.KEYCLOAK_CLIENT_ID
    });
    
    // Additional validation
    if (!decoded.email_verified) {
      return res.status(403).json({ error: 'Email not verified' });
    }
    
    // Attach user info to request
    req.user = {
      id: decoded.sub,
      email: decoded.email,
      name: decoded.name,
      roles: decoded.realm_access?.roles || [],
      scopes: decoded.scope?.split(' ') || []
    };
    
    next();
  } catch (error) {
    console.error('Token validation error:', error.message);
    
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({ error: 'Invalid token' });
    }
    
    res.status(500).json({ error: 'Token validation failed' });
  }
};

// Usage
app.get('/api/manual-protected', validateToken, (req, res) => {
  res.json({ 
    message: 'Manually validated',
    user: req.user 
  });
});

Fastify Integration

Basic Fastify Setup

const fastify = require('fastify')();
const fastifyJWT = require('@fastify/jwt');
const fastifyAuth = require('@fastify/auth');
const axios = require('axios');

// JWT plugin configuration
fastify.register(fastifyJWT, {
  secret: {
    public: async (request, token) => {
      // Fetch public key from Keycloak
      const response = await axios.get(
        `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`
      );
      
      const key = response.data.keys.find(k => k.kid === token.kid);
      if (!key) throw new Error('Key not found');
      
      return key;
    }
  },
  verify: {
    iss: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
    aud: process.env.KEYCLOAK_CLIENT_ID
  }
});

fastify.register(fastifyAuth);

// Authentication decorator
fastify.decorate('authenticate', async (request, reply) => {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.send(err);
  }
});

// Role verification decorator
fastify.decorate('verifyRole', (role) => {
  return async (request, reply) => {
    const roles = request.user.realm_access?.roles || [];
    
    if (!roles.includes(role)) {
      reply.status(403).send({ error: 'Insufficient permissions' });
    }
  };
});

// Routes
fastify.get('/api/public', async (request, reply) => {
  return { message: 'Public route' };
});

fastify.get('/api/protected', {
  preHandler: fastify.auth([fastify.authenticate])
}, async (request, reply) => {
  return { user: request.user };
});

fastify.get('/api/admin', {
  preHandler: fastify.auth([
    fastify.authenticate,
    fastify.verifyRole('admin')
  ])
}, async (request, reply) => {
  return { message: 'Admin route' };
});

// Start server
const start = async () => {
  try {
    await fastify.listen({ port: 3000, host: '0.0.0.0' });
    console.log('Server running on port 3000');
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();

Advanced Fastify with Plugins

// keycloak-plugin.js
const fp = require('fastify-plugin');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

async function keycloakPlugin(fastify, options) {
  const {
    realm,
    authServerUrl,
    clientId,
    bearerOnly = true
  } = options;
  
  // JWKS client
  const client = jwksClient({
    jwksUri: `${authServerUrl}/realms/${realm}/protocol/openid-connect/certs`,
    cache: true,
    cacheMaxEntries: 5,
    cacheMaxAge: 600000
  });
  
  // Get key function
  const getKey = (header, callback) => {
    client.getSigningKey(header.kid, (err, key) => {
      if (err) return callback(err);
      callback(null, key.publicKey || key.rsaPublicKey);
    });
  };
  
  // Verify token function
  const verifyToken = (token) => {
    return new Promise((resolve, reject) => {
      jwt.verify(token, getKey, {
        algorithms: ['RS256'],
        issuer: `${authServerUrl}/realms/${realm}`,
        audience: clientId
      }, (err, decoded) => {
        if (err) reject(err);
        else resolve(decoded);
      });
    });
  };
  
  // Decorate fastify with auth method
  fastify.decorate('keycloakAuth', async function (request, reply) {
    const authHeader = request.headers.authorization;
    
    if (!authHeader) {
      throw fastify.httpErrors.unauthorized('Missing authorization header');
    }
    
    const token = authHeader.replace('Bearer ', '');
    
    try {
      const decoded = await verifyToken(token);
      request.user = decoded;
    } catch (error) {
      throw fastify.httpErrors.unauthorized('Invalid token');
    }
  });
  
  // Add role checking
  fastify.decorate('requireRole', (role) => {
    return async function (request, reply) {
      if (!request.user) {
        throw fastify.httpErrors.unauthorized('Not authenticated');
      }
      
      const roles = request.user.realm_access?.roles || [];
      if (!roles.includes(role)) {
        throw fastify.httpErrors.forbidden('Insufficient permissions');
      }
    };
  });
  
  // Add scope checking
  fastify.decorate('requireScope', (scope) => {
    return async function (request, reply) {
      if (!request.user) {
        throw fastify.httpErrors.unauthorized('Not authenticated');
      }
      
      const scopes = request.user.scope?.split(' ') || [];
      if (!scopes.includes(scope)) {
        throw fastify.httpErrors.forbidden('Missing required scope');
      }
    };
  });
}

module.exports = fp(keycloakPlugin, {
  name: 'fastify-keycloak'
});

// Usage in main app
const fastify = require('fastify')();

fastify.register(require('./keycloak-plugin'), {
  realm: process.env.KEYCLOAK_REALM,
  authServerUrl: process.env.KEYCLOAK_URL,
  clientId: process.env.KEYCLOAK_CLIENT_ID
});

// Protected routes
fastify.get('/api/user', {
  preHandler: [fastify.keycloakAuth]
}, async (request, reply) => {
  return { user: request.user };
});

fastify.get('/api/admin', {
  preHandler: [fastify.keycloakAuth, fastify.requireRole('admin')]
}, async (request, reply) => {
  return { message: 'Admin access granted' };
});

Backend for Frontend (BFF) Pattern

OAuth Flow Implementation

const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const session = require('express-session');

const app = express();

// Session for storing auth state
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Generate PKCE challenge
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
  
  return { verifier, challenge };
}

// OAuth configuration
const oauth = {
  authorizationURL: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
  tokenURL: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
  userInfoURL: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
  clientId: process.env.KEYCLOAK_CLIENT_ID,
  clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
  redirectURI: `${process.env.APP_URL}/auth/callback`
};

// Login route
app.get('/auth/login', (req, res) => {
  const state = crypto.randomBytes(16).toString('base64url');
  const { verifier, challenge } = generatePKCE();
  
  // Store in session
  req.session.oauth = { state, verifier };
  
  // Build authorization URL
  const params = new URLSearchParams({
    client_id: oauth.clientId,
    redirect_uri: oauth.redirectURI,
    response_type: 'code',
    scope: 'openid email profile',
    state: state,
    code_challenge: challenge,
    code_challenge_method: 'S256'
  });
  
  res.redirect(`${oauth.authorizationURL}?${params}`);
});

// Callback route
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Verify state
  if (!req.session.oauth || state !== req.session.oauth.state) {
    return res.status(400).json({ error: 'Invalid state' });
  }
  
  try {
    // Exchange code for tokens
    const tokenResponse = await axios.post(oauth.tokenURL, 
      new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: oauth.clientId,
        client_secret: oauth.clientSecret,
        code: code,
        redirect_uri: oauth.redirectURI,
        code_verifier: req.session.oauth.verifier
      }), {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    const { access_token, refresh_token, id_token } = tokenResponse.data;
    
    // Get user info
    const userResponse = await axios.get(oauth.userInfoURL, {
      headers: {
        'Authorization': `Bearer ${access_token}`
      }
    });
    
    // Store in session
    req.session.user = userResponse.data;
    req.session.tokens = {
      access_token,
      refresh_token,
      id_token
    };
    
    // Clean up OAuth session data
    delete req.session.oauth;
    
    // Redirect to app
    res.redirect('/dashboard');
    
  } catch (error) {
    console.error('OAuth callback error:', error);
    res.status(500).json({ error: 'Authentication failed' });
  }
});

// Refresh token
app.post('/auth/refresh', async (req, res) => {
  if (!req.session.tokens?.refresh_token) {
    return res.status(401).json({ error: 'No refresh token' });
  }
  
  try {
    const response = await axios.post(oauth.tokenURL,
      new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: oauth.clientId,
        client_secret: oauth.clientSecret,
        refresh_token: req.session.tokens.refresh_token
      }), {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    // Update tokens
    req.session.tokens = {
      ...req.session.tokens,
      access_token: response.data.access_token,
      refresh_token: response.data.refresh_token || req.session.tokens.refresh_token
    };
    
    res.json({ success: true });
    
  } catch (error) {
    console.error('Token refresh error:', error);
    res.status(401).json({ error: 'Refresh failed' });
  }
});

// Logout
app.post('/auth/logout', async (req, res) => {
  const refreshToken = req.session.tokens?.refresh_token;
  
  // Destroy session
  req.session.destroy();
  
  // Revoke token at Keycloak (optional)
  if (refreshToken) {
    try {
      await axios.post(
        `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/logout`,
        new URLSearchParams({
          client_id: oauth.clientId,
          client_secret: oauth.clientSecret,
          refresh_token: refreshToken
        })
      );
    } catch (error) {
      console.error('Token revocation error:', error);
    }
  }
  
  res.json({ success: true });
});

// Protected API proxy
app.use('/api', (req, res, next) => {
  if (!req.session.tokens?.access_token) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  
  // Add token to proxied requests
  req.headers.authorization = `Bearer ${req.session.tokens.access_token}`;
  next();
});

// Proxy to backend API
const { createProxyMiddleware } = require('http-proxy-middleware');
app.use('/api', createProxyMiddleware({
  target: process.env.BACKEND_API_URL,
  changeOrigin: true,
  onError: (err, req, res) => {
    if (err.code === 'ECONNRESET' || err.message.includes('401')) {
      // Try to refresh token
      // Implement refresh logic here
    }
    res.status(500).json({ error: 'Proxy error' });
  }
}));

Service-to-Service Authentication

Client Credentials Flow

const axios = require('axios');

class KeycloakServiceClient {
  constructor(config) {
    this.config = config;
    this.token = null;
    this.tokenExpiry = null;
  }
  
  async getToken() {
    // Check if token is still valid
    if (this.token && this.tokenExpiry > Date.now()) {
      return this.token;
    }
    
    try {
      const response = await axios.post(
        `${this.config.authServerUrl}/realms/${this.config.realm}/protocol/openid-connect/token`,
        new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: this.config.clientId,
          client_secret: this.config.clientSecret,
          scope: this.config.scope || ''
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }
      );
      
      this.token = response.data.access_token;
      // Set expiry with 60 second buffer
      this.tokenExpiry = Date.now() + (response.data.expires_in - 60) * 1000;
      
      return this.token;
    } catch (error) {
      console.error('Failed to get service token:', error);
      throw new Error('Service authentication failed');
    }
  }
  
  async makeAuthenticatedRequest(url, options = {}) {
    const token = await this.getToken();
    
    return axios({
      ...options,
      url,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`
      }
    });
  }
}

// Usage
const serviceClient = new KeycloakServiceClient({
  authServerUrl: process.env.KEYCLOAK_URL,
  realm: process.env.KEYCLOAK_REALM,
  clientId: process.env.SERVICE_CLIENT_ID,
  clientSecret: process.env.SERVICE_CLIENT_SECRET
});

// Make authenticated service calls
async function callOtherService() {
  try {
    const response = await serviceClient.makeAuthenticatedRequest(
      'https://other-service.example.com/api/data',
      {
        method: 'GET'
      }
    );
    
    return response.data;
  } catch (error) {
    console.error('Service call failed:', error);
    throw error;
  }
}

WebSocket Authentication

Socket.io with Keycloak

const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const app = express();
const httpServer = createServer(app);

// JWKS client for token validation
const client = jwksClient({
  jwksUri: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`
});

const getKey = (header, callback) => {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) return callback(err);
    callback(null, key.publicKey || key.rsaPublicKey);
  });
};

// Socket.io setup with auth
const io = new Server(httpServer, {
  cors: {
    origin: process.env.CLIENT_URL,
    credentials: true
  }
});

// Authentication middleware
io.use(async (socket, next) => {
  try {
    const token = socket.handshake.auth.token;
    
    if (!token) {
      return next(new Error('Authentication error'));
    }
    
    // Verify token
    jwt.verify(token, getKey, {
      algorithms: ['RS256'],
      issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
      audience: process.env.KEYCLOAK_CLIENT_ID
    }, (err, decoded) => {
      if (err) {
        return next(new Error('Authentication error'));
      }
      
      // Attach user info to socket
      socket.userId = decoded.sub;
      socket.user = {
        id: decoded.sub,
        email: decoded.email,
        name: decoded.name,
        roles: decoded.realm_access?.roles || []
      };
      
      next();
    });
  } catch (err) {
    next(new Error('Authentication error'));
  }
});

// Connection handling
io.on('connection', (socket) => {
  console.log(`User ${socket.user.email} connected`);
  
  // Join user to their personal room
  socket.join(`user:${socket.userId}`);
  
  // Join role-based rooms
  socket.user.roles.forEach(role => {
    socket.join(`role:${role}`);
  });
  
  // Handle events
  socket.on('message', (data) => {
    // Validate permissions before processing
    if (socket.user.roles.includes('chat_user')) {
      io.to('role:chat_user').emit('message', {
        user: socket.user.name,
        message: data.message,
        timestamp: new Date()
      });
    }
  });
  
  socket.on('disconnect', () => {
    console.log(`User ${socket.user.email} disconnected`);
  });
});

// Client-side connection
/*
const socket = io('http://localhost:3000', {
  auth: {
    token: localStorage.getItem('access_token')
  }
});

socket.on('connect', () => {
  console.log('Connected to server');
});

socket.on('connect_error', (error) => {
  if (error.message === 'Authentication error') {
    // Refresh token and retry
  }
});
*/

httpServer.listen(3000);

Error Handling and Monitoring

Comprehensive Error Handling

const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');

// Logger setup
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new ElasticsearchTransport({
      level: 'info',
      clientOpts: { node: process.env.ELASTICSEARCH_URL },
      index: 'keycloak-auth'
    })
  ]
});

// Auth error handler middleware
const authErrorHandler = (err, req, res, next) => {
  // Log error details
  logger.error('Authentication error', {
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    ip: req.ip,
    user: req.user?.id
  });
  
  // Handle specific auth errors
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({
      error: 'Unauthorized',
      message: 'Invalid or expired token'
    });
  }
  
  if (err.name === 'InsufficientScopeError') {
    return res.status(403).json({
      error: 'Forbidden',
      message: 'Insufficient permissions'
    });
  }
  
  // Default error response
  res.status(500).json({
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'An error occurred'
  });
};

// Metrics collection
const promClient = require('prom-client');

const authCounter = new promClient.Counter({
  name: 'auth_requests_total',
  help: 'Total number of authentication requests',
  labelNames: ['method', 'status']
});

const authDuration = new promClient.Histogram({
  name: 'auth_duration_seconds',
  help: 'Authentication request duration',
  labelNames: ['method']
});

// Metrics middleware
const metricsMiddleware = (req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    
    if (req.path.startsWith('/auth')) {
      authCounter.inc({
        method: req.method,
        status: res.statusCode
      });
      
      authDuration.observe({
        method: req.method
      }, duration);
    }
  });
  
  next();
};

app.use(metricsMiddleware);
app.use(authErrorHandler);

// Metrics endpoint
app.get('/metrics', (req, res) => {
  res.set('Content-Type', promClient.register.contentType);
  res.end(promClient.register.metrics());
});

Testing

Unit Testing Authentication

const request = require('supertest');
const jwt = require('jsonwebtoken');
const app = require('./app');

// Mock Keycloak token
function createMockToken(payload = {}) {
  return jwt.sign({
    sub: 'test-user-id',
    email: '[email protected]',
    name: 'Test User',
    realm_access: { roles: ['user'] },
    ...payload
  }, 'test-secret', { algorithm: 'HS256' });
}

describe('Authentication Tests', () => {
  describe('Protected Routes', () => {
    it('should reject requests without token', async () => {
      const response = await request(app)
        .get('/api/protected')
        .expect(401);
      
      expect(response.body.error).toBe('No authorization header');
    });
    
    it('should accept valid token', async () => {
      const token = createMockToken();
      
      const response = await request(app)
        .get('/api/protected')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);
      
      expect(response.body.user.email).toBe('[email protected]');
    });
    
    it('should reject expired token', async () => {
      const token = createMockToken({ exp: Math.floor(Date.now() / 1000) - 3600 });
      
      await request(app)
        .get('/api/protected')
        .set('Authorization', `Bearer ${token}`)
        .expect(401);
    });
  });
  
  describe('Role-Based Access', () => {
    it('should allow admin access to admin routes', async () => {
      const token = createMockToken({
        realm_access: { roles: ['user', 'admin'] }
      });
      
      await request(app)
        .get('/api/admin')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);
    });
    
    it('should deny non-admin access to admin routes', async () => {
      const token = createMockToken({
        realm_access: { roles: ['user'] }
      });
      
      const response = await request(app)
        .get('/api/admin')
        .set('Authorization', `Bearer ${token}`)
        .expect(403);
      
      expect(response.body.error).toBe('Insufficient permissions');
    });
  });
});

Integration Testing

const axios = require('axios');
const { startServer, stopServer } = require('./server');

describe('Keycloak Integration Tests', () => {
  let server;
  let keycloakToken;
  
  beforeAll(async () => {
    server = await startServer();
    
    // Get real token from Keycloak
    const response = await axios.post(
      `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
      new URLSearchParams({
        grant_type: 'password',
        client_id: process.env.KEYCLOAK_CLIENT_ID,
        client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
        username: 'test-user',
        password: 'test-password'
      })
    );
    
    keycloakToken = response.data.access_token;
  });
  
  afterAll(async () => {
    await stopServer(server);
  });
  
  it('should authenticate with real Keycloak token', async () => {
    const response = await axios.get('http://localhost:3000/api/user', {
      headers: {
        'Authorization': `Bearer ${keycloakToken}`
      }
    });
    
    expect(response.status).toBe(200);
    expect(response.data.user.email).toBeDefined();
  });
});

Best Practices

1. Security

  • Always validate tokens on every request
  • Use HTTPS in production
  • Implement proper CORS configuration
  • Store sensitive config in environment variables
  • Rotate client secrets regularly

2. Performance

  • Cache JWKS keys
  • Implement token caching where appropriate
  • Use connection pooling for Redis/database
  • Monitor token validation performance

3. Error Handling

  • Provide clear error messages
  • Log authentication failures
  • Implement retry logic for token refresh
  • Handle edge cases gracefully

4. Monitoring

  • Track authentication metrics
  • Monitor token expiration patterns
  • Alert on unusual authentication patterns
  • Log security events

Troubleshooting

Common Issues

Token Validation Fails

// Debug token validation
const decoded = jwt.decode(token, { complete: true });
console.log('Token header:', decoded.header);
console.log('Token payload:', decoded.payload);

// Verify JWKS endpoint
const jwksResponse = await axios.get(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/certs`);
console.log('Available keys:', jwksResponse.data.keys.map(k => k.kid));

CORS Issues

// Proper CORS configuration
app.use(cors({
  origin: function (origin, callback) {
    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Session Issues

// Debug session
app.use((req, res, next) => {
  console.log('Session ID:', req.sessionID);
  console.log('Session data:', req.session);
  next();
});

Next Steps