Files
bincio-auth/OIDC_PLAN.md
T

6.4 KiB
Raw Blame History

bincio-auth → OIDC Identity Provider

Goal: make bincio-auth a proper OpenID Connect (OIDC) Identity Provider so every bincio service (activity, wiki, Gitea, mobile apps, future tools) delegates auth to it. One account, one password, self-service email reset, no manual user sync per service.

Status: planning


Architecture

bincio-auth becomes the IdP. Every other service is a client.

User → Gitea / activity / wiki / mobile
     → "not logged in → go to bincio.org/oauth2/authorize"
     → user logs in once at bincio.org
     → redirected back with short-lived auth code
     → client POSTs code → bincio-auth returns id_token (RS256 JWT)
     → client verifies token locally via JWKS (no shared secret needed)

Signing: RS256 asymmetric key pair.
Private key on bincio-auth only. Public key published at /.well-known/jwks.json.
Clients verify tokens without any shared secret — truly decoupled.


New endpoints (all additive — nothing existing changes)

Endpoint Purpose
GET /.well-known/openid-configuration OIDC discovery — Gitea and clients read this
GET /.well-known/jwks.json Public RSA key for token verification
GET /oauth2/authorize Login + consent page, issues short-lived auth code
POST /oauth2/token Exchanges auth code for id_token + access_token
GET /oauth2/userinfo Returns profile from access_token

New DB tables

-- Registered OAuth2 clients (Gitea, bincio-activity, mobile app, ...)
CREATE TABLE oauth2_clients (
    client_id       TEXT PRIMARY KEY,
    client_secret   TEXT,          -- NULL for public PKCE clients (mobile)
    name            TEXT NOT NULL,
    redirect_uris   TEXT NOT NULL, -- JSON array of allowed redirect URIs
    scopes          TEXT NOT NULL DEFAULT 'openid profile',
    created_at      INTEGER NOT NULL
);

-- Short-lived single-use authorization codes (expire in 5 min)
CREATE TABLE oauth2_codes (
    code                    TEXT PRIMARY KEY,
    client_id               TEXT NOT NULL,
    handle                  TEXT NOT NULL,
    redirect_uri            TEXT NOT NULL,
    scope                   TEXT NOT NULL,
    code_challenge          TEXT,  -- PKCE (mobile clients)
    code_challenge_method   TEXT,  -- 'S256'
    created_at              INTEGER NOT NULL,
    expires_at              INTEGER NOT NULL,
    used_at                 INTEGER
);

Migration phases

Phase 0 — Preparation [ ]

  • Generate RSA key pair, store private key at /etc/bincio/oidc_private.pem
  • Choose and configure SMTP provider (Postmark or Brevo free tier — VPS IPs often blocked by Gmail/Outlook)
  • Register initial OAuth2 clients in DB: Gitea, bincio-activity (client_id + secret)
  • Decide on bincio.org as the OIDC issuer URL

Phase 1 — OIDC endpoints in bincio-auth [ ]

Additive — nothing existing changes. bincio-activity and wiki keep working unchanged.

  • Add oauth2_clients and oauth2_codes tables to DB schema
  • GET /.well-known/openid-configuration — static JSON describing all endpoints + issuer
  • GET /.well-known/jwks.json — RSA public key in JWK format
  • GET /oauth2/authorize — shows login form (or reuses existing session), issues auth code
  • POST /oauth2/token — validates code, returns id_token (RS256 JWT) + access_token
  • GET /oauth2/userinfo — returns sub, name, preferred_username, custom claims
  • PKCE support (code_challenge / code_verifier) for public clients
  • Smoke test: complete a full OIDC flow with curl / a test script

Phase 2 — Gitea [ ]

  • Install Gitea on VPS (systemd service, port 3000)
  • nginx server block for git.bincio.org with Let's Encrypt cert
  • Configure Gitea: disable self-registration, set admin account
  • Add "Sign in with bincio" — Generic OAuth2 source pointing to bincio-auth OIDC
  • Test: existing bincio users sign in → Gitea auto-provisions their profile
  • Mirror bincio repos (bincio-activity, bincio-auth, bincio-autarchive, bincio-rec, bincio-wiki)

Phase 3 — Migrate bincio-activity to RS256 [ ]

The only phase with live-service risk — handled with a feature flag.

  • bincio-activity fetches JWKS from bincio-auth and caches the public key
  • Add RS256 validation path alongside existing HS256 path (feature flag: --oidc-issuer)
  • Deploy with both paths active, verify RS256 works for all users
  • Remove HS256 path + BINCIO_AUTH_JWT_SECRET env var
  • Rollback plan: keep --jwt-secret flag functional until RS256 is confirmed stable

Phase 4 — Email / SMTP [ ]

  • SMTP config in bincio-auth (--smtp-host, --smtp-user, --smtp-password, --smtp-from, env vars)
  • Self-service password reset: user requests via email → gets link → resets without admin
  • Keep admin-code endpoint as fallback for users without a registered email
  • Optional: email verification on registration

Phase 5 — Mobile app (PKCE) [ ]

  • Register mobile app as a public OIDC client (no client_secret, PKCE only)
  • App opens browser to /oauth2/authorize, handles redirect back via deep link
  • Exchange code for id_token via /oauth2/token (PKCE verifier instead of client_secret)
  • Remove current /api/auth/token mobile workaround

What stays the same throughout

  • bincio.org/login/ — same login page, same UX until Phase 4
  • All existing sessions remain valid during Phases 12
  • bincio-activity users notice nothing during Phases 12
  • bincio_session cookie name and domain unchanged

Risk register

Risk Mitigation
Phase 3 breaks bincio-activity login Feature flag: run HS256 + RS256 in parallel for one deploy cycle
RSA key lost / corrupted Back up /etc/bincio/oidc_private.pem offsite; all active sessions would be invalidated but users just log in again
SMTP deliverability from VPS IP Use a dedicated sending service (Postmark/Brevo), not raw VPS SMTP
Gitea DB diverges from bincio-auth Gitea is read-only consumer — bincio-auth is authoritative; Gitea account = auto-provisioned on first OIDC login

Effort estimate

Phase Estimate
0 — prep ~1 hour
1 — OIDC endpoints ~1.52 days
2 — Gitea ~23 hours
3 — bincio-activity RS256 ~23 hours
4 — email/SMTP ~34 hours
5 — mobile PKCE ~1 day