Files
d4rkbot/docs/sso.md
T
D4rkst3randClaude Fable 5 70cae85064 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>
2026-07-31 13:24:03 +02:00

3.8 KiB

Single Sign-On mit d4rkbot

Der Bot ist Identity-Provider für die anderen D4RKST3R-Dienste (Kanban, Platform …). Statt in jeder App einen eigenen Discord-OAuth2-Flow zu bauen, fragst du beim Bot nach — und bekommst dabei gleich die Discord-Rollen des Users mitgeliefert.

1. App registrieren

Setup → APISingle Sign-On:

Feld Beispiel Bedeutung
Kürzel kanban interner Slug, taucht in der URL auf
Name Kanban-Board Anzeigename
Rücksprung-URL https://kanban.d4rkst3r.de/ erlaubte Ziele (mehrere kommagetrennt)

Beim Anlegen wird ein Secret angezeigt — einmalig, danach nie wieder. In der App als Umgebungsvariable hinterlegen (SSO_SECRET), niemals ins Repo.

Der Bot leitet ausschließlich auf Adressen zurück, die mit einer der hinterlegten Rücksprung-URLs beginnen. Das verhindert, dass jemand den Login-Flow auf eine fremde Seite umbiegt und den Token abgreift.

2. Ablauf

Nutzer klickt "Login"  →  https://bot.d4rkst3r.de/sso/authorize
                              ?app=kanban
                              &redirect=https://kanban.d4rkst3r.de/auth/callback

   ↓ (Bot prüft Session, ggf. erst Discord-Login)

Zurück zu  https://kanban.d4rkst3r.de/auth/callback?sso_token=<token>

   ↓ (App tauscht den Token serverseitig ein)

POST https://bot.d4rkst3r.de/sso/verify
     Authorization: Bearer <SSO_SECRET>
     { "token": "<token>" }

Der Token ist 60 Sekunden gültig und nur einmal einlösbar. Er selbst enthält keine Nutzerdaten — die gibt es erst beim Verify-Aufruf, der serverseitig passieren muss.

3. Antwort von /sso/verify

{
  "user": {
    "id": "123456789012345678",
    "username": "d4rkst3r",
    "displayName": "Alexander",
    "avatar": "https://cdn.discordapp.com/avatars/…",
    "joinedAt": "2026-02-01T12:00:00.000Z"
  },
  "roles": [
    { "id": "111…", "name": "Entwickler" },
    { "id": "222…", "name": "Playtester" }
  ],
  "owner": false,
  "scopes": ["content", "server"],
  "app": "kanban"
}
  • owner — ist der Server-Owner (ADMIN_DISCORD_ID)
  • scopes — Team-Bereiche aus dem Bot (['*'] beim Owner, sonst z. B. ['content'])
  • roles — alle Discord-Rollen; damit kannst du in der App Rechte vergeben, ohne eine eigene Nutzerverwaltung zu pflegen

4. Beispiel (Node / Express)

const BOT = 'https://bot.d4rkst3r.de';

app.get('/login', (req, res) => {
    const redirect = `${process.env.PUBLIC_URL}/auth/callback`;
    res.redirect(`${BOT}/sso/authorize?app=kanban&redirect=${encodeURIComponent(redirect)}`);
});

app.get('/auth/callback', async (req, res) => {
    const result = await fetch(`${BOT}/sso/verify`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${process.env.SSO_SECRET}`,
        },
        body: JSON.stringify({ token: req.query.sso_token }),
    });
    if (!result.ok) return res.status(401).send('Login fehlgeschlagen');

    const { user, roles, owner } = await result.json();
    req.session.user = {
        id: user.id,
        name: user.displayName ?? user.username,
        avatar: user.avatar,
        // Rechte direkt aus den Discord-Rollen ableiten
        canEdit: owner || roles.some((r) => r.name === 'Entwickler'),
    };
    res.redirect('/');
});

5. Hinweise

  • Der Verify-Aufruf muss serverseitig erfolgen — das Secret darf nie im Browser landen.
  • Nur Mitglieder des konfigurierten Discord-Servers können sich anmelden; alle anderen landen auf der Beitritts-Seite des Bots.
  • Rollen werden bei jedem Login frisch von Discord geholt. Wer eine Rolle verliert, verliert die Rechte beim nächsten Login — für sofortige Wirkung die Sitzung in der App kurz halten oder periodisch neu prüfen.
  • Wird eine App im Setup gelöscht, funktioniert ihr Login sofort nicht mehr.