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.

121 linesCodeBlameRaw
1/**
2 * Single sign-on: a workspace's own identity provider, over OpenID
3 * Connect, OAuth 2.0 or SAML 2.0, on every paid plan. Mirrors
4 * `crates/contracts/src/sso.rs`, which says how it works: it never signs
5 * anyone in to g1t; a person signed in to g1t proves themselves to the
6 * workspace's provider to use the workspace.
7 */
8import type { ServiceBinding } from "./clients";
9import type { User } from "./identity";
10import type { Result } from "./result";
11
12export type SsoProtocol = "oidc" | "oauth2" | "saml";
13
14/** What a connection needs, by protocol; it holds no secret. */
15export type SsoConfig = {
16 issuer?: string | null;
17 client_id?: string | null;
18 scopes?: string | null;
19 authorization_url?: string | null;
20 token_url?: string | null;
21 userinfo_url?: string | null;
22 subject_claim?: string | null;
23 email_claim?: string | null;
24 idp_entity_id?: string | null;
25 idp_sso_url?: string | null;
26 certificates?: string[];
27};
28
29export type SsoConnection = {
30 id: string;
31 protocol: SsoProtocol;
32 config: SsoConfig;
33 has_client_secret: boolean;
34 /** SAML's signing certificates, by SHA-256 fingerprint. */
35 certificates: { fingerprint: string }[];
36 allow_join: boolean;
37 tested_at: string | null;
38 /** The username of whoever first signed in through it. */
39 tested_by: string | null;
40 created_at: string;
41 updated_at: string;
42};
43
44export type SsoIdentity = {
45 username: string;
46 display_username?: string | null;
47 subject: string;
48 email: string | null;
49 linked_at: string;
50 last_used_at: string;
51};
52
53export type SsoSettings = {
54 slug: string;
55 connection: SsoConnection | null;
56 required: boolean;
57 /** Whether the owner asking has signed in through it, which requiring it needs. */
58 you_linked: boolean;
59 identities: SsoIdentity[];
60};
61
62export type SsoSummary = {
63 enabled: boolean;
64 protocol: SsoProtocol | null;
65 required: boolean;
66 allow_join: boolean;
67};
68
69export type SsoFinished = { slug: string; next: string; joined: boolean; first: boolean };
70
71export type SaveSso = {
72 protocol: SsoProtocol;
73 config: SsoConfig;
74 /** Empty keeps the one saved. */
75 client_secret?: string | null;
76 /** SAML certificates as pasted (PEM); empty keeps the ones saved. */
77 certificate?: string | null;
78 allow_join: boolean;
79};
80
81async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
82 const response = await service.fetch(`https://service/rpc/${method}`, {
83 method: "POST",
84 headers: { "content-type": "application/json" },
85 body: JSON.stringify(args),
86 });
87 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
88 return (await response.json()) as T;
89}
90
91/** Single sign-on: methods of the identity service. */
92export function ssoClient(identity: ServiceBinding) {
93 const call = <T>(method: string, args: object) => rpc<T>(identity, method, args);
94 return {
95 settings: (actor: User, slug: string) => call<Result<SsoSettings>>("sso_settings", { actor, slug }),
96 summary: (slug: string) => call<SsoSummary>("sso_summary", { slug }),
97 save: (actor: User, slug: string, input: SaveSso) =>
98 call<Result<SsoSettings>>("save_sso", { actor, slug, surface: "web", ...input }),
99 setRequired: (actor: User, slug: string, required: boolean) =>
100 call<Result<boolean>>("set_sso_required", { actor, slug, required, surface: "web" }),
101 remove: (actor: User, slug: string) => call<Result<boolean>>("delete_sso", { actor, slug, surface: "web" }),
102 unlink: (actor: User, slug: string, username: string) =>
103 call<Result<boolean>>("unlink_sso_identity", { actor, slug, username, surface: "web" }),
104 start: (input: { user: User; session_token: string; slug: string; redirect_uri: string; entity_id?: string | null; next: string }) =>
105 call<Result<{ url: string; state: string | null }>>("sso_start", input),
106 finish: (input: { slug: string; session_token: string; state: string; code: string }) =>
107 call<Result<SsoFinished>>("sso_finish", input),
108 saml: (slug: string, samlResponse: string) => call<Result<SsoFinished>>("sso_saml", { slug, saml_response: samlResponse }),
109 metadata: (entityId: string, acsUrl: string) => call<string>("sso_metadata", { entity_id: entityId, acs_url: acsUrl }),
110 };
111}
112
113/**
114 * Where a workspace's provider is pointed, from the site's origin: g1t's
115 * entity id (its metadata's address), the assertion consumer service, and
116 * the callback for OpenID Connect and OAuth 2.0.
117 */
118export function ssoAddresses(origin: string, slug: string) {
119 const base = `${origin.replace(/\/+$/, "")}/auth/sso/${encodeURIComponent(slug.toLowerCase())}`;
120 return { entityId: `${base}/metadata`, metadataUrl: `${base}/metadata`, acsUrl: `${base}/saml`, callbackUrl: `${base}/callback` };
121}