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:
@@ -15,7 +15,9 @@ import {
|
||||
saveTemplate, listTemplates, deleteTemplate,
|
||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||
logAudit, listAudit, playerHistory, deleteUserData,
|
||||
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
||||
} from '../db.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||
import { sanitizeEmbed } from './api-v1.js';
|
||||
@@ -1310,6 +1312,37 @@ ${rssItems}
|
||||
return { entries: listAudit(100) };
|
||||
});
|
||||
|
||||
// --- SSO-Apps (nur Owner): Fremd-Dienste, die den Discord-Login mitnutzen ---
|
||||
|
||||
app.get('/api/ssoapps', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { apps: listSsoApps() };
|
||||
});
|
||||
|
||||
app.post('/api/ssoapps', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
const slug = String(request.body?.slug ?? '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
const name = String(request.body?.name ?? '').trim();
|
||||
const prefix = String(request.body?.redirect_prefix ?? '').trim();
|
||||
if (!slug || !name) return reply.code(400).send({ error: 'slug und name nötig' });
|
||||
if (!prefix.split(/[\s,]+/).filter(Boolean).every((p) => /^https?:\/\/.+/.test(p))) {
|
||||
return reply.code(400).send({ error: 'redirect_prefix: http(s)-URL(s) erwartet' });
|
||||
}
|
||||
if (getSsoApp(slug)) return reply.code(409).send({ error: 'slug schon vergeben' });
|
||||
|
||||
const secret = `sso_${crypto.randomBytes(24).toString('hex')}`;
|
||||
saveSsoApp({ slug, name, redirect_prefix: prefix, secret });
|
||||
logAudit(getSessionUser(request), 'sso-app angelegt', `${name} (${slug})`);
|
||||
// Secret nur in dieser einen Antwort!
|
||||
return { slug, name, redirect_prefix: prefix, secret };
|
||||
});
|
||||
|
||||
app.delete('/api/ssoapps/:slug', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
logAudit(getSessionUser(request), 'sso-app gelöscht', String(request.params.slug));
|
||||
return { deleted: deleteSsoApp(String(request.params.slug)), apps: listSsoApps() };
|
||||
});
|
||||
|
||||
// --- API-Keys (Admin) — für /api/v1/* ---
|
||||
|
||||
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];
|
||||
|
||||
Reference in New Issue
Block a user