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:
@@ -161,6 +161,10 @@ export default function Settings({ me }) {
|
||||
const [botDesc, setBotDesc] = useState('');
|
||||
const [team, setTeam] = useState([]);
|
||||
const [audit, setAudit] = useState([]);
|
||||
// SSO-Apps (Fremd-Dienste mit Discord-Login)
|
||||
const [ssoApps, setSsoApps] = useState([]);
|
||||
const [ssoDraft, setSsoDraft] = useState({ slug: '', name: '', redirect_prefix: '' });
|
||||
const [createdSso, setCreatedSso] = useState(null);
|
||||
const [teamDraft, setTeamDraft] = useState({ user_id: '', scopes: [] });
|
||||
|
||||
const canTab = (id) => {
|
||||
@@ -203,6 +207,7 @@ export default function Settings({ me }) {
|
||||
apiGet('/api/gameservers').then((d) => setServers(d.servers)).catch(() => {});
|
||||
apiGet('/api/webadmins').then((d) => setTeam(d.admins)).catch(() => {});
|
||||
apiGet('/api/auditlog').then((d) => setAudit(d.entries)).catch(() => {});
|
||||
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
|
||||
}, [me.admin, me.scopes?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -236,6 +241,28 @@ export default function Settings({ me }) {
|
||||
if (res) setTeam(res.admins);
|
||||
}
|
||||
|
||||
async function createSsoApp() {
|
||||
if (!ssoDraft.slug.trim() || !ssoDraft.name.trim() || !ssoDraft.redirect_prefix.trim()) {
|
||||
flash('✗ Kürzel, Name und Rücksprung-URL nötig');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await apiPost('/api/ssoapps', ssoDraft);
|
||||
setCreatedSso(res);
|
||||
setSsoDraft({ slug: '', name: '', redirect_prefix: '' });
|
||||
setSsoApps((await apiGet('/api/ssoapps')).apps);
|
||||
flash('✓ App registriert');
|
||||
} catch (e) {
|
||||
flash(e.status === 409 ? '✗ Kürzel schon vergeben' : '✗ Registrierung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSsoApp(slug) {
|
||||
if (!window.confirm(`App „${slug}" entfernen? Der Login über diese App funktioniert danach nicht mehr.`)) return;
|
||||
const res = await apiDelete(`/api/ssoapps/${slug}`).catch(() => null);
|
||||
if (res) setSsoApps(res.apps);
|
||||
}
|
||||
|
||||
const serverNeedsUrl = ['fivem', 'http'].includes(serverDraft.type);
|
||||
async function saveServer() {
|
||||
if (!serverDraft.name.trim() || (serverNeedsUrl ? !serverDraft.query_url.trim() : !serverDraft.host.trim())) {
|
||||
@@ -1325,6 +1352,7 @@ export default function Settings({ me }) {
|
||||
);
|
||||
|
||||
const tabApi = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// API-Keys (/api/v1)</h2>
|
||||
{keys.length > 0 && (
|
||||
@@ -1373,6 +1401,52 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Single Sign-On</h2>
|
||||
<p className="section-intro">
|
||||
Andere Dienste (Kanban, Platform …) können den Discord-Login mitnutzen: Sie
|
||||
leiten auf <code>/sso/authorize</code> weiter und tauschen den Token per
|
||||
<code>POST /sso/verify</code> gegen Nutzerdaten samt Discord-Rollen ein.
|
||||
</p>
|
||||
{ssoApps.length > 0 && (
|
||||
<div className="key-list">
|
||||
{ssoApps.map((a) => (
|
||||
<div className="key-row" key={a.slug}>
|
||||
<span className="key-name">{a.name}</span>
|
||||
<span className="key-scopes">{a.slug}</span>
|
||||
<span className="key-used" title={a.redirect_prefix}>
|
||||
{a.last_used ? `zuletzt: ${a.last_used.slice(0, 16)}` : 'nie benutzt'}
|
||||
</span>
|
||||
<button className="card-delete" title="App entfernen" onClick={() => removeSsoApp(a.slug)}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{createdSso && (
|
||||
<div className="key-created">
|
||||
<p>
|
||||
App <strong>{createdSso.name}</strong> registriert — Secret <strong>jetzt kopieren</strong>,
|
||||
es wird nie wieder angezeigt:
|
||||
</p>
|
||||
<code>{createdSso.secret}</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label>Neue App <span className="field-hint">Kürzel, Anzeigename und erlaubte Rücksprung-URL(s)</span></label>
|
||||
<div className="field-row">
|
||||
<input type="text" style={{ flex: 'none', width: '9rem' }} placeholder="kanban" value={ssoDraft.slug} onChange={(e) => setSsoDraft({ ...ssoDraft, slug: e.target.value })} />
|
||||
<input type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Kanban-Board" value={ssoDraft.name} onChange={(e) => setSsoDraft({ ...ssoDraft, name: e.target.value })} />
|
||||
<input type="text" placeholder="https://kanban.d4rkst3r.de/" value={ssoDraft.redirect_prefix} onChange={(e) => setSsoDraft({ ...ssoDraft, redirect_prefix: e.target.value })} />
|
||||
<button className="btn" onClick={createSsoApp}>Registrieren</button>
|
||||
</div>
|
||||
<p className="field-hint" style={{ marginTop: '.4rem' }}>
|
||||
Mehrere URLs kommagetrennt. Der Bot leitet nur auf Ziele zurück, die mit
|
||||
einem dieser Präfixe beginnen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const tabServer = (
|
||||
|
||||
Reference in New Issue
Block a user