OAuth sign-in: Google + Microsoft (code flow + PKCE, stdlib only) #21

Merged
Cordy merged 6 commits from feat/oauth into main 2026-08-24 00:08:00 +00:00
Owner

Social sign-in for the customer area (enter.html), per Nikola's request.

Endpoints: GET /v1/oauth/providers (enabled list, drives the site buttons), GET /v1/oauth/{provider}/start (302 to the provider with PKCE S256 + state + nonce), GET /v1/oauth/{provider}/callback (code exchange → session token → 302 back to enter.html#oauth=<token>&email=…; errors go to #oauth-error=<code>; everything sensitive travels in URL fragments, never query strings).

Design, matching house constraints: stdlib only — the code flow is hand-rolled; no JWT signature verification needed because the id_token comes straight from the provider's token endpoint over TLS (OIDC Core 3.1.3.7), with iss/aud/nonce/exp still validated. State store is memory-only, 10-min TTL, single-use, like sessions.

Account rules:

  • identity key provider:sub stored on the account (OAuth []string); a linked identity signs into its account even if the provider-side email later changes
  • auto-create/auto-link by verified email only: Google email_verified; Microsoft = MSA consumer tenant, or work tenants only with the xms_edov optional claim (blocks the nOAuth tenant-admin email spoof)
  • unverified → #oauth-error=unverified (new) / exists (existing account, no link)
  • OAuth-created accounts have no password (login always fails); the reset-email flow can set one; org membership via the normal ensureOrgLocked

Config (env, all optional — no env means no buttons, zero behaviour change):
OAUTH_GOOGLE_CLIENT_ID/SECRET, OAUTH_MICROSOFT_CLIENT_ID/SECRET, OAUTH_PUBLIC_ORIGIN (default https://license.swisscairn.ch), OAUTH_SITE_ORIGIN (default site URL).

Tests cover: account creation, verified linking, unverified refusal (both paths), MS verification rules (MSA + xms_edov), state single-use, nonce mismatch, identity-wins-over-email-change, providers list.

Social sign-in for the customer area (enter.html), per Nikola's request. **Endpoints:** `GET /v1/oauth/providers` (enabled list, drives the site buttons), `GET /v1/oauth/{provider}/start` (302 to the provider with PKCE S256 + state + nonce), `GET /v1/oauth/{provider}/callback` (code exchange → session token → 302 back to `enter.html#oauth=<token>&email=…`; errors go to `#oauth-error=<code>`; everything sensitive travels in URL fragments, never query strings). **Design, matching house constraints:** stdlib only — the code flow is hand-rolled; no JWT signature verification needed because the id_token comes straight from the provider's token endpoint over TLS (OIDC Core 3.1.3.7), with iss/aud/nonce/exp still validated. State store is memory-only, 10-min TTL, single-use, like sessions. **Account rules:** - identity key `provider:sub` stored on the account (`OAuth []string`); a linked identity signs into its account even if the provider-side email later changes - auto-create/auto-link **by verified email only**: Google `email_verified`; Microsoft = MSA consumer tenant, or work tenants only with the `xms_edov` optional claim (blocks the nOAuth tenant-admin email spoof) - unverified → `#oauth-error=unverified` (new) / `exists` (existing account, no link) - OAuth-created accounts have no password (login always fails); the reset-email flow can set one; org membership via the normal `ensureOrgLocked` **Config (env, all optional — no env means no buttons, zero behaviour change):** `OAUTH_GOOGLE_CLIENT_ID/SECRET`, `OAUTH_MICROSOFT_CLIENT_ID/SECRET`, `OAUTH_PUBLIC_ORIGIN` (default `https://license.swisscairn.ch`), `OAUTH_SITE_ORIGIN` (default site URL). Tests cover: account creation, verified linking, unverified refusal (both paths), MS verification rules (MSA + xms_edov), state single-use, nonce mismatch, identity-wins-over-email-change, providers list.
Cordy merged commit 872f53fe5c into main 2026-08-24 00:08:00 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-license-server#21
No description provided.