// SECURE: Express.js app with proper JWT secret management
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const winston = require('winston');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();
// SECURE: Environment variable validation
const requiredEnvVars = ['JWT_SECRET', 'JWT_REFRESH_SECRET', 'JWT_ISSUER', 'JWT_AUDIENCE'];
const missingVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingVars.length > 0) {
console.error('Missing required environment variables:', missingVars);
process.exit(1);
}
// Validate secret strength
if (process.env.JWT_SECRET.length < 32 || process.env.JWT_REFRESH_SECRET.length < 32) {
console.error('JWT secrets must be at least 32 characters long');
process.exit(1);
}
// SECURE: Logger configuration without secret exposure
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf(info => {
// Filter out any potential secrets from logs
const filtered = { ...info };
['password', 'secret', 'token', 'key'].forEach(field => {
if (filtered[field]) {
filtered[field] = '[REDACTED]';
}
});
return JSON.stringify(filtered);
})
),
transports: [
new winston.transports.File({ filename: 'app.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Security middleware
app.use(helmet());
app.use(express.json({ limit: '10mb' }));
// Rate limiting for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: { error: 'Too many authentication attempts, please try again later' },
standardHeaders: true,
legacyHeaders: false
});
// SECURE: Token service class
class TokenService {
constructor() {
this.jwtSecret = process.env.JWT_SECRET;
this.refreshSecret = process.env.JWT_REFRESH_SECRET;
this.issuer = process.env.JWT_ISSUER;
this.audience = process.env.JWT_AUDIENCE;
this.blacklistedTokens = new Set(); // In production, use Redis
}
generateTokenPair(user) {
const tokenId = require('crypto').randomUUID();
const accessToken = jwt.sign(
{
userId: user.id,
username: user.username,
role: user.role,
tokenType: 'access',
jti: tokenId
},
this.jwtSecret,
{
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
issuer: this.issuer,
audience: this.audience
}
);
const refreshToken = jwt.sign(
{
userId: user.id,
tokenType: 'refresh',
jti: tokenId
},
this.refreshSecret,
{
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
issuer: this.issuer
}
);
return { accessToken, refreshToken, tokenId };
}
verifyAccessToken(token) {
if (this.blacklistedTokens.has(token)) {
throw new Error('Token has been revoked');
}
return jwt.verify(token, this.jwtSecret, {
issuer: this.issuer,
audience: this.audience
});
}
verifyRefreshToken(token) {
if (this.blacklistedTokens.has(token)) {
throw new Error('Token has been revoked');
}
return jwt.verify(token, this.refreshSecret, {
issuer: this.issuer
});
}
revokeToken(token) {
this.blacklistedTokens.add(token);
// In production, store in Redis with TTL
}
}
const tokenService = new TokenService();
// SECURE: Login endpoint
app.post('/login', authLimiter, async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
logger.warn('Login attempt with missing credentials', {
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.status(400).json({ error: 'Username and password required' });
}
const user = await User.findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
logger.warn('Failed login attempt', {
username: username,
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.status(401).json({ error: 'Invalid credentials' });
}
const { accessToken, refreshToken, tokenId } = tokenService.generateTokenPair(user);
// SECURE: Success logging without exposing tokens or secrets
logger.info('User login successful', {
userId: user.id,
username: user.username,
tokenId: tokenId,
ip: req.ip
});
// Set refresh token in httpOnly cookie for better security
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({
accessToken,
user: {
id: user.id,
username: user.username,
role: user.role
},
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
});
} catch (error) {
// SECURE: Error logging without exposing secrets
logger.error('Login endpoint error', {
error: error.message,
ip: req.ip,
// Never log tokens or secrets
});
res.status(500).json({ error: 'Internal server error' });
}
});
// SECURE: Token refresh endpoint
app.post('/refresh', async (req, res) => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
const decoded = tokenService.verifyRefreshToken(refreshToken);
// Revoke old refresh token
tokenService.revokeToken(refreshToken);
// Get user data and generate new tokens
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
const { accessToken, refreshToken: newRefreshToken, tokenId } = tokenService.generateTokenPair(user);
// Set new refresh token in cookie
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
logger.info('Token refresh successful', {
userId: user.id,
newTokenId: tokenId
});
res.json({ accessToken });
} catch (error) {
logger.error('Token refresh failed', {
error: error.message,
errorType: error.name
});
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Refresh token expired' });
}
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// SECURE: Authentication middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
logger.warn('Authentication attempt without token', {
ip: req.ip,
path: req.path
});
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = tokenService.verifyAccessToken(token);
if (decoded.tokenType !== 'access') {
throw new Error('Invalid token type');
}
req.user = {
userId: decoded.userId,
username: decoded.username,
role: decoded.role
};
// SECURE: Success logging without token or secret exposure
logger.debug('Token authentication successful', {
userId: decoded.userId,
path: req.path
});
next();
} catch (error) {
// SECURE: Error logging without exposing tokens or secrets
logger.warn('Token authentication failed', {
error: error.message,
errorType: error.name,
ip: req.ip,
path: req.path
});
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
} else if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token', code: 'INVALID_TOKEN' });
}
res.status(401).json({ error: 'Authentication failed' });
}
}
// SECURE: Logout endpoint
app.post('/logout', authenticateToken, (req, res) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
const refreshToken = req.cookies.refreshToken;
// Revoke both tokens
if (token) tokenService.revokeToken(token);
if (refreshToken) tokenService.revokeToken(refreshToken);
// Clear refresh token cookie
res.clearCookie('refreshToken');
logger.info('User logout successful', {
userId: req.user.userId
});
res.json({ message: 'Logged out successfully' });
});
// SECURE: Protected route example
app.get('/profile', authenticateToken, async (req, res) => {
try {
const user = await User.findById(req.user.userId).select('-password');
res.json(user);
} catch (error) {
logger.error('Profile fetch failed', {
userId: req.user.userId,
error: error.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
// SECURE: Health check endpoint without exposing secrets
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || 'unknown',
environment: process.env.NODE_ENV || 'unknown'
// Never include secrets or tokens in health checks
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
// SECURE: Startup logging without exposing secrets
logger.info('Server started', {
port: PORT,
environment: process.env.NODE_ENV,
version: process.env.APP_VERSION
// Never log secrets during startup
});
});