Aller au contenu

Node.js Integration Guide

Ce contenu n’est pas encore disponible dans votre langue.

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.

  • Node.js 18+ installed
  • A Keycloak instance (Skycloak account)
  • Basic understanding of OAuth 2.0/OpenID Connect
  • Your Keycloak realm and client configured
# 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
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');
  });
});

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 })
);

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 
  });
});
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();
// 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' };
});
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' });
  }
}));
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;
  }
}
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);
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());
});
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: 'test@example.com',
    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('test@example.com');
    });
    
    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');
    });
  });
});
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();
  });
});
  • Always validate tokens on every request
  • Use HTTPS in production
  • Implement proper CORS configuration
  • Store sensitive config in environment variables
  • Rotate client secrets regularly
  • Cache JWKS keys
  • Implement token caching where appropriate
  • Use connection pooling for Redis/database
  • Monitor token validation performance
  • Provide clear error messages
  • Log authentication failures
  • Implement retry logic for token refresh
  • Handle edge cases gracefully
  • Track authentication metrics
  • Monitor token expiration patterns
  • Alert on unusual authentication patterns
  • Log security events

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();
});