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