162 lines · 6.9 KB
import { env } from "cloudflare:workers";
import { z } from "zod";
import { emailAllowed, isBlockedOutbound } from "./naming";
import { isForgeHostname, sitesDomain } from "./config";
import type { SiteMeta } from "./config";

type OidcConfig = NonNullable<SiteMeta["oidc"]>;

const DISCOVERY_CACHE_SECONDS = 3600;
const CLOCK_SKEW_SECONDS = 60;

const discoverySchema = z.object({
  issuer: z.string(),
  authorization_endpoint: z.string(),
  token_endpoint: z.string(),
  jwks_uri: z.string(),
});
type Discovery = z.infer<typeof discoverySchema>;

const jwkSchema = z.object({ kid: z.string().optional(), kty: z.string(), alg: z.string().optional() }).passthrough();
const claimsSchema = z.object({
  iss: z.string(),
  aud: z.union([z.string(), z.array(z.string())]),
  exp: z.number(),
  nonce: z.string().optional(),
  email: z.string().optional(),
  email_verified: z.union([z.boolean(), z.string()]).optional(),
});

/** Only public HTTPS endpoints; a site owner must not be able to point the dispatcher at internal hosts. */
export function assertPublicHttps(raw: string): URL {
  const url = new URL(raw);
  const blocked = [new URL(env.PUBLIC_URL).hostname, sitesDomain()].filter((host): host is string => Boolean(host));
  if (url.protocol !== "https:" || isBlockedOutbound(url, blocked) || isForgeHostname(url.hostname)) {
    throw new Error(`OIDC endpoint ${url.hostname} must be a public https URL`);
  }
  return url;
}

async function getJson(url: string, init?: RequestInit): Promise<unknown> {