Repository navigation
Expand file tree
/
Copy pathauth-token.ts
More file actions
61 lines (54 loc) · 2.21 KB
/
Copy pathauth-token.ts
File metadata and controls
61 lines (54 loc) · 2.21 KB
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
import { timingSafeEqual } from "node:crypto";
/**
* Result of validating an HTTP request's `Authorization` header against the
* configured bearer token allow-list.
*/
export type AuthTokenValidation =
| { ok: true }
| { ok: false; status: 401; message: string };
const BEARER_PREFIX = "Bearer ";
/**
* Constant-time string equality. `timingSafeEqual` throws on unequal-length
* buffers, so unequal lengths are rejected up front — this leaks only the
* length of the configured token, not which bytes matched, which is the same
* trade-off `timingSafeEqual` itself makes.
*/
function constantTimeEqual(a: string, b: string): boolean {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return timingSafeEqual(bufA, bufB);
}
/**
* Bearer token auth for the HTTP transport (issue #66): "anyone with the
* server URL can access the database." An empty `tokens` list means auth is
* disabled — configuring a token is itself the opt-in (see
* `resolveAuthTokens()` in `src/config/env.ts`), so there is no separate
* "--require-auth" flag to forget to set.
*
* This intentionally stops at a shared-secret allow-list rather than
* implementing the MCP spec's full OAuth 2.1 resource-server model (RFC 9728
* protected-resource metadata, authorization-server discovery, dynamic client
* registration, PKCE). That machinery solves multi-tenant identity
* federation; DBHub's actual gap is coarser — "is this request from someone
* who has the secret," not "who is this user and what scopes do they have."
*/
export function validateAuthToken(
authorizationHeader: string | undefined,
tokens: string[]
): AuthTokenValidation {
if (tokens.length === 0) return { ok: true };
if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
return {
ok: false,
status: 401,
message: "Missing or malformed Authorization header. Expected: Bearer <token>",
};
}
const presented = authorizationHeader.slice(BEARER_PREFIX.length);
const matches = tokens.some((token) => constantTimeEqual(presented, token));
if (!matches) {
return { ok: false, status: 401, message: "Invalid bearer token" };
}
return { ok: true };
}