File size: 2,657 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import {
  AUTHZ_HEADER_AUTH_ID,
  AUTHZ_HEADER_AUTH_KIND,
  AUTHZ_HEADER_AUTH_LABEL,
  AUTHZ_HEADER_AUTH_SCOPES,
  AUTHZ_HEADER_ROUTE_CLASS,
} from "./headers";
import type { AuthSubject, RouteClass } from "./types";

export class AuthzAssertionError extends Error {
  readonly status: number;
  readonly code: string;

  constructor(code: string, message: string, status = 500) {
    super(message);
    this.name = "AuthzAssertionError";
    this.code = code;
    this.status = status;
  }
}

type HeaderSource = Headers | { get(name: string): string | null };

function readHeader(source: HeaderSource, name: string): string | null {
  return source.get(name) ?? null;
}

function isHeaderSource(value: unknown): value is HeaderSource {
  if (value instanceof Headers) return true;
  if (typeof value !== "object" || value === null) return false;
  return typeof (value as { get?: unknown }).get === "function";
}

export function readSubjectFromHeaders(headers: HeaderSource): AuthSubject {
  const kind = (readHeader(headers, AUTHZ_HEADER_AUTH_KIND) ?? "anonymous") as AuthSubject["kind"];
  const id = readHeader(headers, AUTHZ_HEADER_AUTH_ID) ?? "anonymous";
  const label = readHeader(headers, AUTHZ_HEADER_AUTH_LABEL) ?? undefined;
  const rawScopes = readHeader(headers, AUTHZ_HEADER_AUTH_SCOPES);
  const scopes = rawScopes
    ? rawScopes
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean)
    : [];
  return { kind, id, label, scopes };
}

export function readRouteClassFromHeaders(headers: HeaderSource): RouteClass | null {
  const raw = readHeader(headers, AUTHZ_HEADER_ROUTE_CLASS);
  if (raw === "PUBLIC" || raw === "CLIENT_API" || raw === "MANAGEMENT") return raw;
  return null;
}

export function assertAuth(
  request: Request | { headers: HeaderSource },
  expected: RouteClass
): AuthSubject {
  const headers = request.headers;

  if (!isHeaderSource(headers)) {
    throw new AuthzAssertionError("AUTHZ_INVALID_REQUEST", "Request headers are unavailable", 500);
  }

  const actualClass = readRouteClassFromHeaders(headers);
  if (!actualClass) {
    throw new AuthzAssertionError(
      "AUTHZ_NOT_INITIALIZED",
      "Request did not pass through the authz middleware",
      500
    );
  }

  if (actualClass !== expected) {
    throw new AuthzAssertionError(
      "AUTHZ_ROUTE_CLASS_MISMATCH",
      `Expected ${expected} but got ${actualClass}`,
      500
    );
  }

  const subject = readSubjectFromHeaders(headers);

  if (expected !== "PUBLIC" && subject.kind === "anonymous") {
    throw new AuthzAssertionError("AUTHZ_UNAUTHENTICATED", "Authentication required", 401);
  }

  return subject;
}