# 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 ```sql -- 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 1–2 - bincio-activity users notice nothing during Phases 1–2 - `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.5–2 days | | 2 — Gitea | ~2–3 hours | | 3 — bincio-activity RS256 | ~2–3 hours | | 4 — email/SMTP | ~3–4 hours | | 5 — mobile PKCE | ~1 day |