Files
zesdex/src/interfaces/api/middleware/auth.ts
T

52 lines
1.9 KiB
TypeScript

/**
* JWT authentication middleware for the API.
* Mirrors `apps/interfaces/api/src/middleware/auth.rs`.
*
* Validates the `Authorization: Bearer <token>` header and injects the
* validated subject into `req.user`. Refresh tokens are rejected on
* protected routes — only access tokens are accepted.
*/
import { verifyToken } from "@zesdex/infrastructure";
import type { ApiState } from "../state.ts";
/** Claims decoded from a valid JWT, attached to the request. */
export interface JwtClaims {
sub: string;
}
/** Result of authentication: either the subject or an error status/detail. */
export type AuthResult = { ok: true; sub: string } | { ok: false; status: number; detail: string };
/**
* Authenticate a request against the API state's JWT secret.
* Reads the `Authorization: Bearer <token>` header, verifies the signature
* and token type (must be `access`), and returns the subject on success.
*/
export function authenticateRequest(state: ApiState, headers: Headers): AuthResult {
const authHeader = headers.get("Authorization");
if (!authHeader) {
return { ok: false, status: 401, detail: "Missing or invalid Authorization header" };
}
const prefix = "Bearer ";
if (!authHeader.startsWith(prefix)) {
return { ok: false, status: 401, detail: "Missing or invalid Authorization header" };
}
const token = authHeader.slice(prefix.length).trim();
if (!token) {
return { ok: false, status: 401, detail: "Missing or invalid Authorization header" };
}
try {
const claims = verifyToken(state.jwt_secret, token);
if (claims.typ !== "access") {
return {
ok: false,
status: 401,
detail: "refresh tokens are not accepted on protected routes",
};
}
return { ok: true, sub: claims.sub };
} catch (e) {
return { ok: false, status: 401, detail: `Invalid token: ${(e as Error).message}` };
}
}