MBO-Tech-IT-Webseite/modules/02-admin-auth/files/lib/admin-auth.ts

175 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Web Crypto API kompatibel mit Edge Runtime + Node.js
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: "bestaetigt" | "abgelehnt" | "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!;
// ✅ Token expiration: 2 Stunden (matches cookie maxAge in login route)
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}`;
}
/**
* Auth-Wrapper für Admin-API-Routes.
*
* Prüft den admin_session-Cookie, verifiziert das HMAC-Token und gibt die
* Session zurück ODER eine fertige 401-Response (wenn nicht authentifiziert).
*
* Verwendung am Anfang einer Admin-API-Route:
* const check = await requireAdmin();
* if (check instanceof NextResponse) return check;
* const session = check; // AdminSession
*/
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;
// ✅ Prüfe ob Token revoked wurde
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;
}
}
// ──── Action-Tokens für Email-Links (Anfrage bestätigen/ablehnen/abschließen) ────
export async function createActionToken(
anfrageId: string,
status: "bestaetigt" | "abgelehnt" | "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;
// ✅ Prüfe ob Token bereits verwendet wurde (One-Time-Use)
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;
}
}