docs: OIDC migration plan — bincio-auth becomes full IdP

This commit is contained in:
Davide Scaini
2026-06-03 12:38:35 +02:00
parent 29c1bfe5ba
commit c341c27ad4
+152
View File
@@ -0,0 +1,152 @@
# 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 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 |