63 lines
1.7 KiB
TypeScript
63 lines
1.7 KiB
TypeScript
/**
|
|
* Edge-runtime variant of the signed-session verification used by middleware.
|
|
*
|
|
* Next.js middleware runs on the Edge runtime, where Node's `crypto` module is
|
|
* unavailable, so we verify the HMAC signature with Web Crypto (SubtleCrypto).
|
|
* Keep this in sync with `src/lib/session.ts` (same secret, same algorithm).
|
|
*/
|
|
|
|
function base64url(bytes: ArrayBuffer): string {
|
|
const view = new Uint8Array(bytes);
|
|
let bin = '';
|
|
for (let i = 0; i < view.length; i++) {
|
|
bin += String.fromCharCode(view[i]);
|
|
}
|
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
function timingSafeEqual(a: string, b: string): boolean {
|
|
if (a.length !== b.length) return false;
|
|
let diff = 0;
|
|
for (let i = 0; i < a.length; i++) {
|
|
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
}
|
|
return diff === 0;
|
|
}
|
|
|
|
/**
|
|
* Verify a signed cookie value in the Edge runtime.
|
|
* Returns the user id when the signature is valid, otherwise null.
|
|
*/
|
|
export async function verifySignedUserIdEdge(
|
|
value: string | undefined | null
|
|
): Promise<string | null> {
|
|
if (!value) return null;
|
|
|
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
if (!secret) return null;
|
|
|
|
const separator = value.lastIndexOf('.');
|
|
if (separator <= 0 || separator === value.length - 1) {
|
|
return null;
|
|
}
|
|
|
|
const userId = value.slice(0, separator);
|
|
const providedSig = value.slice(separator + 1);
|
|
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(secret),
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign']
|
|
);
|
|
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(userId));
|
|
const expectedSig = base64url(signature);
|
|
|
|
if (!timingSafeEqual(providedSig, expectedSig)) {
|
|
return null;
|
|
}
|
|
|
|
return userId;
|
|
}
|