128 lines
3.1 KiB
TypeScript
128 lines
3.1 KiB
TypeScript
/**
|
|
* Auth Middleware for MCP Products
|
|
* Verifies JWT tokens and sets user context
|
|
*/
|
|
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import jwt from 'jsonwebtoken';
|
|
import { logger } from '../utils/logger';
|
|
|
|
// JWT configuration (should match mcp-auth config)
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production-min-256-bits';
|
|
|
|
// JWT Payload interface
|
|
export interface JWTPayload {
|
|
sub: string; // user_id
|
|
email: string;
|
|
tenantId: string;
|
|
isOwner: boolean;
|
|
iat: number;
|
|
exp: number;
|
|
}
|
|
|
|
// Extend Express Request to include auth info
|
|
declare global {
|
|
namespace Express {
|
|
interface Request {
|
|
userId?: string;
|
|
tenantId?: string;
|
|
userEmail?: string;
|
|
isOwner?: boolean;
|
|
isAuthenticated?: boolean;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Auth middleware that verifies JWT tokens
|
|
* If valid, sets userId, tenantId, and userEmail on request
|
|
*/
|
|
export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
const authHeader = req.headers.authorization;
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
res.status(401).json({
|
|
error: 'Unauthorized',
|
|
code: 'MISSING_TOKEN',
|
|
message: 'No authentication token provided',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
|
|
|
|
// Set auth info on request
|
|
req.userId = decoded.sub;
|
|
req.tenantId = decoded.tenantId;
|
|
req.userEmail = decoded.email;
|
|
req.isOwner = decoded.isOwner;
|
|
req.isAuthenticated = true;
|
|
|
|
// Also set tenant ID header for RLS queries
|
|
req.headers['x-tenant-id'] = decoded.tenantId;
|
|
req.headers['x-user-id'] = decoded.sub;
|
|
|
|
next();
|
|
} catch (error) {
|
|
if (error instanceof jwt.TokenExpiredError) {
|
|
res.status(401).json({
|
|
error: 'Unauthorized',
|
|
code: 'TOKEN_EXPIRED',
|
|
message: 'Token has expired',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (error instanceof jwt.JsonWebTokenError) {
|
|
res.status(401).json({
|
|
error: 'Unauthorized',
|
|
code: 'INVALID_TOKEN',
|
|
message: 'Invalid token',
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.error('Auth middleware error', { error });
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
code: 'AUTH_ERROR',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optional auth middleware
|
|
* Sets auth info if token is valid, but allows unauthenticated requests
|
|
*/
|
|
export function optionalAuthMiddleware(req: Request, _res: Response, next: NextFunction): void {
|
|
const authHeader = req.headers.authorization;
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
req.isAuthenticated = false;
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
|
|
|
|
req.userId = decoded.sub;
|
|
req.tenantId = decoded.tenantId;
|
|
req.userEmail = decoded.email;
|
|
req.isOwner = decoded.isOwner;
|
|
req.isAuthenticated = true;
|
|
|
|
req.headers['x-tenant-id'] = decoded.tenantId;
|
|
req.headers['x-user-id'] = decoded.sub;
|
|
} catch {
|
|
req.isAuthenticated = false;
|
|
}
|
|
|
|
next();
|
|
}
|