Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions relay/src/middleware/admin-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { Request, Response, NextFunction } from "express";
import { isRateLimited, recordFailedAttempt } from "./token-auth";
import { secureCompare, hashToken } from "../utils/security";
import { authConfig } from "../config";
import { loggers } from "../utils/logger";
Expand All @@ -27,6 +28,19 @@ function getAdminPasswordHash(): string | null {
* Uses timing-safe comparison to prevent timing attacks
*/
export function adminAuthMiddleware(req: Request, res: Response, next: NextFunction): void {
const clientIp = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || "unknown";

if (isRateLimited(clientIp)) {
log.warn(
{ ip: clientIp, path: req.path },
"Admin auth failed - rate limited"
);
res.status(429).json({
success: false,
error: "Too many failed authentication attempts. Please try again later.",
});
return;
}
// Check Authorization header (Bearer token)
const authHeader = req.headers["authorization"];
const bearerToken = authHeader && authHeader.split(" ")[1];
Expand All @@ -45,6 +59,7 @@ export function adminAuthMiddleware(req: Request, res: Response, next: NextFunct
},
"Admin auth failed - no token"
);
recordFailedAttempt(clientIp);
res.status(401).json({
success: false,
error: "Unauthorized - Admin token required",
Expand Down Expand Up @@ -83,6 +98,7 @@ export function adminAuthMiddleware(req: Request, res: Response, next: NextFunct
},
"Admin auth failed - invalid token"
);
recordFailedAttempt(clientIp);
res.status(401).json({
success: false,
error: "Unauthorized - Invalid admin token",
Expand Down
86 changes: 86 additions & 0 deletions relay/src/tests/admin-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { adminAuthMiddleware } from "../middleware/admin-auth";
import { isRateLimited, recordFailedAttempt } from "../middleware/token-auth";
import { Request, Response, NextFunction } from "express";

// Mock dependencies
vi.mock("../utils/logger", () => ({
loggers: {
server: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
},
}));

vi.mock("../config", () => ({
authConfig: { adminPassword: "test-admin-password", strictSessionIp: true },
}));

vi.mock("../middleware/token-auth", () => {
return {
isRateLimited: vi.fn(),
recordFailedAttempt: vi.fn(),
};
});

describe("adminAuthMiddleware rate limiting", () => {
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;
let mockNext: NextFunction;

beforeEach(() => {
vi.useFakeTimers();
mockReq = {
headers: {},
ip: "192.168.1.100",
path: "/api/system/stats",
};
mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
};
mockNext = vi.fn();

vi.clearAllMocks();
});

afterEach(() => {
vi.useRealTimers();
});

it("should block requests when rate limited", () => {
vi.mocked(isRateLimited).mockReturnValueOnce(true);

adminAuthMiddleware(mockReq as Request, mockRes as Response, mockNext);

expect(isRateLimited).toHaveBeenCalledWith(mockReq.ip);
expect(mockRes.status).toHaveBeenCalledWith(429);
expect(mockRes.json).toHaveBeenCalledWith({
success: false,
error: "Too many failed authentication attempts. Please try again later.",
});
expect(mockNext).not.toHaveBeenCalled();
});

it("should record failed attempt when no token is provided", () => {
vi.mocked(isRateLimited).mockReturnValueOnce(false);

adminAuthMiddleware(mockReq as Request, mockRes as Response, mockNext);

expect(recordFailedAttempt).toHaveBeenCalledWith(mockReq.ip);
expect(mockRes.status).toHaveBeenCalledWith(401);
});

it("should record failed attempt when an invalid token is provided", () => {
vi.mocked(isRateLimited).mockReturnValueOnce(false);
mockReq.headers = { authorization: "Bearer invalid-token" };

adminAuthMiddleware(mockReq as Request, mockRes as Response, mockNext);

expect(recordFailedAttempt).toHaveBeenCalledWith(mockReq.ip);
expect(mockRes.status).toHaveBeenCalledWith(401);
});
});
Loading