Skip to content
File

Blob: src/worker/auth/oidc.ts

typescript66 lines
1import * as client from "openid-client";
2 
3export function normalizeIssuer(input: string): URL {
4 const url = new URL(input);
5 if (url.search || url.hash || url.username || url.password) {
6 throw new Error("Invalid tessera OIDC issuer URL");
7 }
8 
9 const isLoopback =
10 url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
11 if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
12 throw new Error("Tessera OIDC issuer must use HTTPS outside loopback development");
13 }
14 
15 url.pathname = url.pathname.replace(/\/+$/g, "");
16 return url;
17}
18 
19export async function discoverTessera(env: Env): Promise<client.Configuration> {
20 const issuer = normalizeIssuer(env.TESSERA_OIDC_ISSUER);
21 const options =
22 issuer.protocol === "http:"
23 ? {
24 execute: [client.allowInsecureRequests],
25 }
26 : undefined;
27 
28 return await client.discovery(
29 issuer,
30 env.TESSERA_OIDC_CLIENT_ID,
31 env.TESSERA_OIDC_CLIENT_SECRET,
32 client.ClientSecretBasic(env.TESSERA_OIDC_CLIENT_SECRET),
33 options,
34 );
35}
36 
37export function controlPlaneCallbackUrl(requestUrl: string): string {
38 const url = new URL(requestUrl);
39 url.pathname = "/api/v1/auth/oidc/callback";
40 url.search = "";
41 url.hash = "";
42 return url.toString();
43}
44 
45export function validateReturnTo(value: string | null, requestUrl: string): string {
46 if (!value) return "/";
47 const requestOrigin = new URL(requestUrl).origin;
48 
49 if (value.startsWith("/") && !value.startsWith("//")) return value;
50 
51 const url = new URL(value);
52 if (url.origin !== requestOrigin) throw new Error("Invalid return_to origin");
53 return `${url.pathname}${url.search}${url.hash}`;
54}
55 
56export function profileFromClaims(claims: client.IDToken): {
57 sub: string;
58 email?: string | null;
59 displayName?: string | null;
60} {
61 if (!claims.sub) throw new Error("ID token is missing sub");
62 const email = typeof claims.email === "string" ? claims.email : null;
63 const displayName = typeof claims.name === "string" ? claims.name : email;
64 return { sub: claims.sub, email, displayName };
65}