ADR-002: Centralized Social Login (GitHub/Google) via SystemAuth¶
Status: Accepted
Deciders: @grokify
Relates to: ADR-001 (the SystemAuth / sf_ / sf_principal_id naming this
ADR keys its account-linking on), the OAuth server PRD (docs/design/FEAT_OAUTH_PRD.md,
which listed "social login providers (handled separately)" as a non-goal — this
ADR is where that deferred work lands), and the authentication PRD
(docs/design/FEAT_AUTHN_PRD.md, US-1: "sign in via OAuth (GitHub, Google)").
Executed by INIT-SYSTEMFORGE-005. Consumed by downstream relying-party
applications, which depend on this capability as RMI-SYSTEMFORGE-001.
Context¶
Downstream relying-party applications have referenced a central login
capability — RMI-SYSTEMFORGE-001, "log in with GitHub/Google, built once
centrally" — as a dependency. That capability was never actually specced or
built in this repo: the roadmaps start at RMI-SYSTEMFORGE-003, and the OAuth
server PRD explicitly deferred social login. What exists today is three
disconnected pieces, none of which is a mounted, principal-upserting,
session-establishing social-login server:
session/oauth— a complete GitHub/Google OAuth-dance library (AuthorizationURL→HandleCallback→ fetch user profile via raw HTTP againstapi.github.com/googleapis.com). It returns a*UserInfoand does no persistence. It is dead code: zero import sites across the repo.identity/oauthclient— a second, parallel GitHub/Google user-fetch helper (FetchGitHubUser,FetchGoogleUser,GenerateState,StateManager, provider configs). This is the one a reference consumer actually imports. So the repo ships two overlapping social-login primitive packages.- The SystemAuth server (
cmd/systemauth,identity/systemauth) is a Fosite OAuth2/OIDC authorization server — it issues tokens to downstream clients (/oauth/authorize,/oauth/token,/oauth/introspect,/oauth/revoke,/.well-known/*). It has no "log in with GitHub/Google" routes. Itsfederation.go/federation_handlers.goare SystemAuth↔app SSO federation between our own apps, not upstream social login.
The only working end-to-end GitHub/Google flow lives inside a reference consumer application, reimplemented locally. That reference pattern carries three defects we must not propagate by copy-paste:
- Unvalidated post-login redirect that leaks the access token — the target
is taken straight from the query string with no allowlist, and the access
token is appended as
?access_token=..., landing it in browser history, proxy logs, andRefererheaders. - Stubbed refresh tokens — the refresh handler returns "not implemented; please re-authenticate."
- No-op logout — logout is a bare
204with no session/token revocation.
It also has two inconsistent account-linking models in the same file: the
direct GitHub/Google path keys the local principal by email only and never
records a stable identity id; the SystemAuth-federation path keys by the OIDC
sub → sf_principal_id (with verified-email as a linking fallback). The
second is correct and stable; the first silently merges distinct upstream
identities that happen to share an email and breaks if an email changes.
The identity substrate is ready: sf_principal_id is defined once in the
shared identity/ent/mixin.PrincipalMixin (ADR-001) and is already used
consistently by downstream principal schemas. What is missing is the login
surface and a single sanctioned place to build it.
Decision¶
Make SystemAuth the single identity provider for GitHub/Google login.
Relying-party applications do not implement social login themselves; they
federate to SystemAuth as OIDC clients and link the returned identity by
sf_principal_id. GitHub/Google client credentials live only in the SystemAuth
deployment — one upstream OAuth app registration per provider, not one per
relying party.
Where the login lives¶
- Mount GitHub/Google login on the SystemAuth server. Add login-start and
callback routes to
cmd/systemauth/identity/systemauth. On callback, SystemAuth upserts its ownPrincipaland establishes the hardened__Host-sf_loginsession, then returns the user to the relying party through the OIDC authorization-code flow it already serves. - Account-linking model (canonical): upsert the SystemAuth
Principalkeyed by the upstream provider identity (provider+ provider subject), recorded as the stable principal identity. A verified upstream email may link an incoming login to an existing principal, but email is never the primary key. This is the stable path generalized from the reference consumer's SystemAuth-federation handler; the email-only keying is rejected. - Relying parties link by
sf_principal_id. A relying party's callback find-or-creates its local principal by the OIDCsub(the SystemAuth principal UUID) →sf_principal_id, with verified email as a linking fallback — the one model, provided as a reusable helper so no app reimplements it.
Consolidate the primitives¶
- Collapse
session/oauthandidentity/oauthclientinto one sanctioned social-login primitives package that the SystemAuth server consumes; retire the duplicate. There is exactly one place that speaks to github.com / google.com.
Fix the three gaps centrally¶
Because the flow is built once, the reference-pattern defects are fixed once:
- Allowlist-validated post-login redirect, and never place tokens in a redirect URL — the session is a cookie; the authorization code / tokens flow through the OIDC exchange, not the query string.
- Real refresh-token rotation (reuse-detection, absolute expiry) — the Fosite server already issues refresh tokens; the login/session layer must use them rather than stub them.
- Real logout — session revocation plus token revocation via the existing
/oauth/revoke.
Relying-party contract¶
- Provide the reusable OIDC-client callback + middleware helper (sub →
sf_principal_idfind-or-create, verified-email linking) and the/bff/*cookie-session surface the shared frontend expects, so the browser path is a cookie/BFF contract while programmatic clients (CLI/MCP) use bearer tokens (SystemAuth sessions / per-user API keys) directly against the API. - Serve
/oauth/userinfo. Discovery advertises auserinfo_endpointbut no route is mounted; the relying-party callback needs it (or must fall back to the ID token + JWKS). Mounting it is the cleaner contract and is scoped as an execution item.
Rejected alternative: shared library each app mounts in-process¶
Extract the reference pattern into one shared Go package that each app mounts to do its own GitHub/Google login and upsert its own principal. This needs no running SystemAuth and is the smaller change, but it does not deliver one login / one session across apps (the shell-composition goal), forces every app to register its own upstream OAuth apps and hold those secrets, and leaves N copies of the callback/upsert logic to drift. Rejected in favor of the IdP-centralized model, consistent with the "built once centrally, every relying party consumes it" intent that downstream consumers already depend on.
Consequences¶
- Relying-party applications gain a hard dependency on a running SystemAuth instance for interactive login (programmatic bearer/API-key access is unaffected). This is the deliberate cost of one-login-across-apps.
- The two duplicate primitive packages (
session/oauth,identity/oauthclient) collapse to one; the retired import path is a breaking change for any consumer on the old one, carried in the same coordinated pass as the other identity breaking changes (pre-1.0 semver). - The three reference-pattern defects are fixed once, in SystemAuth, instead of being copied into each consumer.
- Carried cautions (from the identity integration review): the default JWKS has a single non-rotating key id; the OAuth state store defaults to in-memory (single-instance only) and needs a shared store under horizontal scaling; DPoP is bindable but not enforced; upstream is GitHub/Google only (no generic OIDC/SAML yet). These bound the first release and are tracked, not resolved here.
- Per-consumer migration off app-local social login is tracked in INIT-SYSTEMFORGE-005 and intentionally not enumerated in this repo, matching the convention ADR-001 / INIT-SYSTEMFORGE-004 established.
References¶
- ADR-001 (
SystemAuth/sf_/sf_principal_id— the linking identity). docs/design/FEAT_OAUTH_PRD.md(social login deferred as "handled separately");docs/design/FEAT_AUTHN_PRD.mdUS-1 (the requirement).- INIT-SYSTEMFORGE-005 ROADMAP (the execution RMIs).
- Affected surfaces:
session/oauth,identity/oauthclient(consolidate);cmd/systemauth,identity/systemauth/server.go,identity/systemauth/handler_discovery.go(login + userinfo routes, session);identity/ent/mixin.PrincipalMixin(sf_principal_id).