-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmiddleware.ts
More file actions
123 lines (111 loc) · 4.26 KB
/
Copy pathmiddleware.ts
File metadata and controls
123 lines (111 loc) · 4.26 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
import { NextResponse, type NextRequest } from "next/server";
/**
* Universal trust_device cookie kill-switch.
*
* Better-auth's twoFactor plugin honors a 30-day `better-auth.trust_device`
* cookie and silently skips the TOTP challenge on subsequent /sign-in/email
* calls when the cookie validates. The hackathon requirement is "every login
* challenges", so this middleware:
*
* 1. Strips `better-auth.trust_device` (and the `__Secure-` variant) from
* the inbound Cookie header for every /api/auth/* and /api/security/*
* request before better-auth ever sees it. The plugin's `getSignedCookie`
* lookup then returns null and the flow falls into the 2FA challenge path.
*
* 2. Appends `Set-Cookie: ...trust_device=; Max-Age=0` directives to every
* outbound response on those paths, so any cookie already in the browser
* from prior sessions is dropped.
*
* Also enforces wallet-based admin access for /admin routes (excluding the
* /admin/connect page which is the wallet connection entry point).
*/
const TRUST_DEVICE_COOKIE_NAMES = [
"better-auth.trust_device",
"__Secure-better-auth.trust_device"
];
function stripTrustDeviceFromCookieHeader(cookieHeader: string): string {
return cookieHeader
.split(";")
.map((c) => c.trim())
.filter((c) => {
if (!c) return false;
const eq = c.indexOf("=");
const name = eq < 0 ? c : c.slice(0, eq);
return !TRUST_DEVICE_COOKIE_NAMES.includes(name);
})
.join("; ");
}
export function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// ── Admin Protection (cheap presence gate) ───────────────────────────
// Redirect to /admin/connect when neither an admin wallet session nor a
// logged-in user session cookie is present. This is only a UX gate — the
// /admin page itself does the authoritative check: it HMAC-verifies the
// admin_session token (unforgeable) or the ADMIN_EMAILS session.
if (path.startsWith("/admin") && path !== "/admin/connect") {
const hasWalletSession = request.cookies.get("admin_session")?.value;
const hasUserSession =
request.cookies.get("better-auth.session_token")?.value ||
request.cookies.get("__Secure-better-auth.session_token")?.value;
if (!hasWalletSession && !hasUserSession) {
return NextResponse.redirect(new URL("/admin/connect", request.url));
}
}
// Build modified request headers without trust_device cookies, so the route
// handler (and better-auth's internal cookie lookup) cannot read them.
const newRequestHeaders = new Headers(request.headers);
const cookieHeader = newRequestHeaders.get("cookie");
if (cookieHeader) {
const stripped = stripTrustDeviceFromCookieHeader(cookieHeader);
if (stripped) {
newRequestHeaders.set("cookie", stripped);
} else {
newRequestHeaders.delete("cookie");
}
}
const response = NextResponse.next({
request: { headers: newRequestHeaders }
});
// Expire both cookie names on every outbound auth response so the browser
// drops them. Expiring a non-existent cookie is a no-op.
response.cookies.set({
name: "better-auth.trust_device",
value: "",
maxAge: 0,
path: "/",
httpOnly: true,
sameSite: "lax"
});
response.cookies.set({
name: "__Secure-better-auth.trust_device",
value: "",
maxAge: 0,
path: "/",
httpOnly: true,
sameSite: "lax",
secure: true
});
// ── Deception Mode Protection ──────────────────────────────────────
const isHoneyPath = path.startsWith("/honeypot") ||
path.startsWith("/api/honey");
if (isHoneyPath) {
const honeyToken = request.cookies.get("sentinel-deception-mode")?.value;
if (!honeyToken) {
// Attacker trying to deep-link into honeypot without a token
return NextResponse.redirect(new URL("/login", request.url));
}
// Allow honeypot access
return response;
}
return response;
}
export const config = {
// Run on auth-relevant paths, admin routes, and honeypot routes
matcher: [
"/api/auth/:path*",
"/api/security/:path*",
"/admin/:path*",
"/honeypot/:path*",
"/api/honey/:path*"
]
};