Skip to content

Auth & Sessions

Dashboard login — §39.1 #67, designed. SSO-only at MVP (GitHub + Google, FR-005), enforced by the §13 API binary; permissions are a separate concern (RBAC §15). (Design record: specs/2026-07-11-auth-sessions-design.md.)

Login flow

  1. Stardeck sends the user to GET /auth/{provider}/login → Starbase redirects to the provider via the §11 SSOProvider (AuthURL, with state CSRF nonce).
  2. Provider redirects back → ExchangeCode returns the verified profile.
  3. Find-or-create with auto-linking: match sso_identities on (provider, provider_user_id); if none, match users on the verified email and link a new identity to that account; if still none, create the user. One person, one account, any provider — both MVP providers verify emails, which is what makes the email match safe.
  4. Mint the session (below), set cookies, redirect to the dashboard.

Token model

  • Access token: JWT, 15 min, claims kept skinny — sub (user id), sid (session id), iat/exp. Roles and workspace membership are not in the token: the §15.6 middleware resolves the PermissionContext per request, so role changes and removals take effect immediately. Signed HS256 with a key from the §39.1 #18 catalog (the API is the only issuer and verifier at MVP).
  • Refresh token: opaque random value, stored hashed in sessions, rotated on every use — each refresh issues a new token and marks the old row replaced_by. Reuse of a rotated-out token revokes the whole session family (stolen-token detection). Lifetime: 30-day sliding window, 90-day absolute cap.
  • Transport — httpOnly cookies, never JavaScript-readable: Secure; HttpOnly; SameSite=Lax, scoped to .starform.io (Stardeck on app., API on api. — same site, cookies flow on XHR). Customer apps live on starform.app (DNS & Domains) and can never see these cookies. CSRF: SameSite=Lax plus a required custom header on mutating requests.
  • Revocation: logout revokes the session row; "log out everywhere" revokes all of the user's rows; both are immediate for refresh and ≤15 min for in-flight access tokens.

Tables

Auth tables · sessions + SSO identities · Starbase Postgres
sso_identities (
    id               UUID PRIMARY KEY,
    user_id          UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    provider         TEXT NOT NULL CHECK (provider IN ('github', 'google')),
    provider_user_id TEXT NOT NULL,
    email            TEXT NOT NULL,             -- as verified by the provider at link time
    created_at       TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (provider, provider_user_id)
);

sessions (
    id                 UUID PRIMARY KEY,        -- the JWT `sid` claim
    user_id            UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    refresh_token_hash TEXT NOT NULL,           -- SHA-256; plaintext only in the cookie
    family_id          UUID NOT NULL,           -- rotation chain; reuse ⇒ revoke the family
    replaced_by        UUID REFERENCES sessions(id),
    ip                 INET,
    user_agent         TEXT,
    created_at         TIMESTAMPTZ DEFAULT NOW(),
    last_used_at       TIMESTAMPTZ DEFAULT NOW(),
    expires_at         TIMESTAMPTZ NOT NULL,    -- min(last_used + 30 d, created + 90 d)
    revoked_at         TIMESTAMPTZ
);

Deferred (§39.3 #25)

Password reset (N/A while login is SSO-only) · MFA · additional SSO providers (GitLab/Bitbucket interfaces exist, §11).


Cross-references

SSO provider interface → §11 · enforced in the API binary → §13 · permission resolution the sid/sub feed → §15.6 · the rest of the platform schema → Database Schema · why customer apps can't touch these cookies → DNS & Domains. Canonical map: Canonical Sources.