Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

73 linesCodeBlameRaw
1/**
2 * The parts of a workspace's single sign-on that the site itself decides:
3 * the cookies that bind a trip to the provider to this browser, where to
4 * go afterwards, and the one address the Worker answers before React
5 * Router (a SAML response, posted from the provider's site).
6 */
7
8import { safeNext } from "./next.ts";
9
10/** The OAuth state of a trip to a workspace's provider, for ten minutes. */
11export const SSO_STATE_COOKIE = "g1t_sso_state";
12/** Why a SAML sign-in did not work, shown once on the page it lands on. */
13export const SSO_ERROR_COOKIE = "g1t_sso_error";
14/**
15 * Binds a SAML trip to the browser that set off. SameSite=None, so the
16 * provider's cross-site POST carries it back (the session cookie, Lax, is
17 * not sent with it); HttpOnly, and only to /auth/sso.
18 */
19export const SAML_BINDING_COOKIE = "g1t_sso_saml";
20
21/** A `Set-Cookie` value for the binding, for ten minutes; empty clears it. */
22export function bindingCookie(value: string): string {
23 return `${SAML_BINDING_COOKIE}=${value}; Path=/auth/sso; HttpOnly; Secure; SameSite=None; Max-Age=${value ? 600 : 0}`;
24}
25
26/** The binding cookie's value, or null. Hex only. */
27export function readBinding(header: string | null): string | null {
28 for (const part of (header ?? "").split(";")) {
29 const [key, value] = part.trim().split("=");
30 if (key === SAML_BINDING_COOKIE && value && /^[0-9a-f]{64}$/.test(value)) return value;
31 }
32 return null;
33}
34
35/** The workspace (its id, or a name) a `POST /auth/sso/<workspace>/saml` is for, or null. */
36export function samlPath(pathname: string): string | null {
37 const match = /^\/auth\/sso\/([a-z0-9][a-z0-9_-]{0,63})\/saml\/?$/i.exec(pathname);
38 return match ? match[1].toLowerCase() : null;
39}
40
41/** Where to go once signed in through a workspace's provider. */
42export function afterSso(finished: { slug: string; next: string }): string {
43 const next = safeNext(finished.next);
44 return next === "/" ? `/${finished.slug}` : next;
45}
46
47/** A `Set-Cookie` value carrying `message`, for a minute: Lax, HttpOnly. */
48export function errorCookie(message: string): string {
49 const value = encodeURIComponent(message.slice(0, 600));
50 return `${SSO_ERROR_COOKIE}=${value}; Path=/auth/sso; HttpOnly; Secure; SameSite=Lax; Max-Age=60`;
51}
52
53/** Clears that cookie. */
54export function clearErrorCookie(): string {
55 return `${SSO_ERROR_COOKIE}=; Path=/auth/sso; HttpOnly; Secure; SameSite=Lax; Max-Age=0`;
56}
57
58/** The message in that cookie, or null. */
59export function readErrorCookie(header: string | null): string | null {
60 for (const part of (header ?? "").split(";")) {
61 const [key, ...rest] = part.trim().split("=");
62 if (key !== SSO_ERROR_COOKIE) continue;
63 try {
64 return decodeURIComponent(rest.join("=")) || null;
65 } catch {
66 return null;
67 }
68 }
69 return null;
70}
71
72/** How each protocol is named on the page. */
73export const PROTOCOL_NAMES = { oidc: "OpenID Connect", oauth2: "OAuth 2.0", saml: "SAML 2.0" } as const;