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:
+112
@@ -0,0 +1,112 @@
|
||||
# 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 → **API** → *Single 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`
|
||||
|
||||
```json
|
||||
{
|
||||
"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)
|
||||
|
||||
```js
|
||||
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.
|
||||
Reference in New Issue
Block a user