52 lines
1.9 KiB
TypeScript
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}` };
|
|
}
|
|
}
|