Single Sign-On: der Bot als Identity-Provider für die anderen Dienste

Andere Apps (Kanban, Platform …) nutzen ab sofort den Discord-Login des
Bots mit, statt jeweils eigenes OAuth zu bauen — und bekommen die
Discord-Rollen des Users gleich mitgeliefert.

Ablauf: App leitet auf /sso/authorize weiter, der Bot prüft die Session
(ggf. erst Discord-Login) und schickt einen signierten Token zurück, den
die App serverseitig per POST /sso/verify gegen die Nutzerdaten tauscht.

Sicherheit:
- Rücksprung-Ziele müssen einem registrierten Präfix entsprechen
  (kein Open Redirect, kein Token-Abgriff über fremde Hosts)
- Token HMAC-signiert, 60 Sekunden gültig, nur einmal einlösbar
- Verify braucht das App-Secret (timing-safe verglichen)
- SSO bleibt Mitgliedern des Discord-Servers vorbehalten
- App-Verwaltung ist Owner-only, Secret wird nur einmal angezeigt

Dazu: sso_apps-Tabelle, Verwaltung im API-Tab, Rücksprung nach dem
Login (return-Cookie, nur interne Pfade), Anleitung in docs/sso.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 13:24:03 +02:00
co-authored by Claude Fable 5
parent 8853bce2ec
commit 70cae85064
8 changed files with 413 additions and 2 deletions
+25
View File
@@ -516,6 +516,31 @@ export function deleteUserData(userId) {
return result;
}
// SSO: Fremd-Apps (Kanban, Platform …), die den Discord-Login des Bots mitnutzen
db.exec(`
CREATE TABLE IF NOT EXISTS sso_apps (
slug TEXT PRIMARY KEY,
name TEXT NOT NULL,
redirect_prefix TEXT NOT NULL,
secret TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_used TEXT
);
`);
const upsertSsoApp = db.prepare(`
INSERT INTO sso_apps (slug, name, redirect_prefix, secret) VALUES (@slug, @name, @redirect_prefix, @secret)
ON CONFLICT(slug) DO UPDATE SET name = excluded.name, redirect_prefix = excluded.redirect_prefix
`);
const getSsoAppStmt = db.prepare('SELECT * FROM sso_apps WHERE slug = ?');
const listSsoAppsStmt = db.prepare('SELECT slug, name, redirect_prefix, created_at, last_used FROM sso_apps ORDER BY name');
const deleteSsoAppStmt = db.prepare('DELETE FROM sso_apps WHERE slug = ?');
const touchSsoAppStmt = db.prepare(`UPDATE sso_apps SET last_used = datetime('now') WHERE slug = ?`);
export const saveSsoApp = (app) => upsertSsoApp.run(app);
export const getSsoApp = (slug) => getSsoAppStmt.get(slug) ?? null;
export const listSsoApps = () => listSsoAppsStmt.all();
export const deleteSsoApp = (slug) => deleteSsoAppStmt.run(slug).changes > 0;
export const touchSsoApp = (slug) => touchSsoAppStmt.run(slug);
// Audit-Log: wer (Owner/Team) hat wann was im Webinterface geändert
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (