-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
148 lines (125 loc) · 4.56 KB
/
Copy pathmiddleware.ts
File metadata and controls
148 lines (125 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
import { safeEqual } from "@/lib/dispatch-env";
type AuthMode = "basic" | "oidc" | "disabled" | undefined;
function getAuthMode(): AuthMode {
const mode = process.env.DISPATCH_AUTH_MODE;
if (mode === "basic" || mode === "oidc" || mode === "disabled") return mode;
return undefined;
}
function shouldUseSecureAuthCookie(request: NextRequest): boolean {
const authUrl = process.env.AUTH_URL ?? process.env.NEXTAUTH_URL;
if (authUrl) {
try {
return new URL(authUrl).protocol === "https:";
} catch {
return false;
}
}
return request.nextUrl.protocol === "https:";
}
function isBearerAuthorized(authHeader: string | null): boolean {
const token = process.env.DISPATCH_AGENT_TOKEN;
if (!token) return false;
const match = /^Bearer\s+(.+)$/i.exec(authHeader ?? "");
return match ? safeEqual(match[1].trim(), token) : false;
}
function parseBasicCredentials(authHeader: string | null): { username: string; password: string } | null {
const match = /^Basic\s+(.+)$/i.exec(authHeader ?? "");
if (!match) return null;
try {
const decoded = atob(match[1]);
const colonIndex = decoded.indexOf(":");
if (colonIndex === -1) return null;
return {
username: decoded.slice(0, colonIndex),
password: decoded.slice(colonIndex + 1),
};
} catch {
return null;
}
}
function isBasicAuthorized(authHeader: string | null): boolean {
const expectedUsername = process.env.DISPATCH_AUTH_USERNAME;
const expectedPassword = process.env.DISPATCH_AUTH_PASSWORD;
if (!expectedUsername || !expectedPassword) return false;
const credentials = parseBasicCredentials(authHeader);
return Boolean(
credentials &&
safeEqual(credentials.username, expectedUsername) &&
safeEqual(credentials.password, expectedPassword)
);
}
/**
* Next.js middleware that enforces Basic Auth when DISPATCH_AUTH_MODE="basic".
*
* Auth mode behavior:
* - "basic" : HTTP Basic Auth required for UI routes. API routes also allow
* DISPATCH_AGENT_TOKEN Bearer auth for agents and workers.
* - "oidc" : OIDC session required for UI routes. API routes authorize via
* route handlers so Bearer auth and session cookies both work.
* - "disabled" : No auth enforcement at all.
* - undefined : Legacy mode — no middleware enforcement; routes handle their own auth.
*/
export async function middleware(request: NextRequest) {
const authMode = getAuthMode();
const isApiRoute = request.nextUrl.pathname.startsWith("/api/");
// "disabled" mode — no enforcement
if (authMode === "disabled") {
return NextResponse.next();
}
if (authMode === "oidc") {
if (isApiRoute) {
return NextResponse.next();
}
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
secureCookie: shouldUseSecureAuthCookie(request),
});
if (token) {
return NextResponse.next();
}
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname + request.nextUrl.search);
return NextResponse.redirect(loginUrl);
}
// No auth mode set (legacy) — no middleware enforcement; routes handle their own auth
if (!authMode) {
return NextResponse.next();
}
const authHeader = request.headers.get("authorization");
if (isApiRoute && (isBearerAuthorized(authHeader) || isBasicAuthorized(authHeader))) {
return NextResponse.next();
}
// "basic" mode — enforce Basic Auth on operator UI routes
if (isBasicAuthorized(authHeader)) {
return NextResponse.next();
}
// No valid Basic Auth header — reject
return unauthorizedResponse(request);
}
/**
* Build a 401 Unauthorized response.
* For API routes, returns JSON. For UI pages, triggers the browser auth dialog.
*/
function unauthorizedResponse(_request: NextRequest): NextResponse {
const response = new NextResponse(null, { status: 401 });
response.headers.set("WWW-Authenticate", 'Basic realm="Dispatch", charset="UTF-8"');
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api/auth/* (NextAuth OIDC routes)
* - api/health (health check, always public)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - /login (public login page)
*/
"/((?!api/auth|api/health|_next/static|_next/image|favicon\\.ico|login).*)",
],
};