MBO-Tech-IT-Webseite/lib/admin-auth.ts

146 lines
4.0 KiB
TypeScript

import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { isSessionTokenRevoked, isActionTokenUsed } from "./token-blacklist";
const encoder = new TextEncoder();
interface AdminSession {
id: string;
email: string;
name: string;
exp: number;
}
interface ActionToken {
anfrageId: string;
status: "in_bearbeitung" | "abgeschlossen";
exp: number;
}
function toBase64Url(buffer: ArrayBuffer | Uint8Array): string {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function fromBase64Url(str: string): ArrayBuffer {
const b64 = str.replace(/-/g, "+").replace(/_/g, "/");
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return bytes.buffer as ArrayBuffer;
}
async function getKey(secret: string): Promise<CryptoKey> {
return crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
}
export async function createSessionToken(
session: Omit<AdminSession, "exp">
): Promise<string> {
const secret = process.env.ADMIN_SESSION_TOKEN!;
const payload: AdminSession = {
...session,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 2,
};
const data = toBase64Url(encoder.encode(JSON.stringify(payload)));
const key = await getKey(secret);
const sig = toBase64Url(await crypto.subtle.sign("HMAC", key, encoder.encode(data)));
return `${data}.${sig}`;
}
export async function requireAdmin(): Promise<AdminSession | NextResponse> {
const cookieStore = await cookies();
const token = cookieStore.get("admin_session")?.value;
if (!token) {
return NextResponse.json({ error: "Nicht authentifiziert" }, { status: 401 });
}
const session = await verifySessionToken(token);
if (!session) {
return NextResponse.json({ error: "Session ungültig" }, { status: 401 });
}
return session;
}
export async function verifySessionToken(token: string): Promise<AdminSession | null> {
try {
const secret = process.env.ADMIN_SESSION_TOKEN!;
const [data, sig] = token.split(".");
if (!data || !sig) return null;
if (await isSessionTokenRevoked(sig)) return null;
const key = await getKey(secret);
const valid = await crypto.subtle.verify(
"HMAC",
key,
fromBase64Url(sig),
encoder.encode(data)
);
if (!valid) return null;
const session: AdminSession = JSON.parse(
new TextDecoder().decode(new Uint8Array(fromBase64Url(data)))
);
if (session.exp < Math.floor(Date.now() / 1000)) return null;
return session;
} catch {
return null;
}
}
export async function createActionToken(
anfrageId: string,
status: "in_bearbeitung" | "abgeschlossen",
ablaufTage = 7
): Promise<string> {
const secret = process.env.ADMIN_SESSION_TOKEN!;
const payload: ActionToken = {
anfrageId,
status,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * ablaufTage,
};
const data = toBase64Url(encoder.encode(JSON.stringify(payload)));
const key = await getKey(secret);
const sig = toBase64Url(await crypto.subtle.sign("HMAC", key, encoder.encode(data)));
return `${data}.${sig}`;
}
export async function verifyActionToken(
token: string
): Promise<{ anfrageId: string; status: string } | null> {
try {
const secret = process.env.ADMIN_SESSION_TOKEN!;
const [data, sig] = token.split(".");
if (!data || !sig) return null;
if (await isActionTokenUsed(sig)) return null;
const key = await getKey(secret);
const valid = await crypto.subtle.verify(
"HMAC",
key,
fromBase64Url(sig),
encoder.encode(data)
);
if (!valid) return null;
const actionToken: ActionToken = JSON.parse(
new TextDecoder().decode(new Uint8Array(fromBase64Url(data)))
);
if (actionToken.exp < Math.floor(Date.now() / 1000)) return null;
return { anfrageId: actionToken.anfrageId, status: actionToken.status };
} catch {
return null;
}
}