Das Dashboard wird echt: Next.js und Discord-OAuth
Von der Skizze zur laufenden Weboberfläche: Anmeldung über Discord, Serverauswahl, Einstellungen. Und die drei Sicherheitsfragen, die ich vorher nicht auf dem Zettel hatte.
1073 Wörter · 5 Min. Lesezeit
- yurna
- web
- nextjs
Im Februar habe ich angefangen, das Dashboard tatsächlich zu bauen. Die Skizze lag seit Dezember, die Fragen waren durchdacht, und die Antwort auf „womit" fiel nach zwei Abenden Vergleich auf Next.js — vor allem, weil ich damit Server- und Clientteil in einem Projekt hatte und mich nicht um zwei Deployments kümmern musste.
Die Anmeldung
Discord bietet OAuth2 an, und der Ablauf ist Standard: Nutzer wird zu Discord geschickt, bestätigt, kommt mit einem Code zurück, den man serverseitig gegen ein Token tauscht.
Wichtig sind die angefragten Bereiche. Ich brauche drei:
identify Wer ist das?
guilds Auf welchen Servern ist die Person?
guilds.join (optional) Person einem Server hinzufügen
Was ich bewusst nicht anfrage: email, connections, messages.read. Jeder zusätzliche Bereich erscheint im Zustimmungsdialog und muss begründet sein. Ich habe seitdem eine einfache Regel: Wenn ich einem Nutzer nicht in einem Satz erklären kann, wofür eine Berechtigung nötig ist, frage ich sie nicht an.
Für die Sitzungsverwaltung habe ich eine fertige Bibliothek genommen statt selbst zu bauen. Sitzungsverwaltung ist eines der Themen, bei denen eigene Kreativität fast nur Nachteile hat.
Die drei Sicherheitsfragen, die ich unterschätzt habe
Erstens: Wem gehört die Berechtigungsangabe? Die Serverliste, die man von Discord bekommt, enthält für jeden Server ein Berechtigungsfeld. Naheliegend ist, daraus abzuleiten, wer Einstellungen ändern darf. Der Haken: Diese Liste ist ein Schnappschuss vom Moment der Anmeldung. Wenn jemandem danach die Rechte entzogen werden, steht in seiner Sitzung weiterhin, dass er darf — und Sitzungen laufen bei mir dreißig Tage.
Die Lösung: Bei jeder verändernden Aktion wird serverseitig nachgeprüft, ob die Person auf diesem Server tatsächlich noch die nötigen Rechte hat. Nicht aus der Sitzung, sondern frisch.
Zweitens: Wo liegen die Zugangstoken? Nach der Anmeldung hat man ein Zugriffstoken des Nutzers. Das gehört nicht in die Sitzung, die im Browser landet, und schon gar nicht in eine Antwort, die der Client abrufen kann. Meine erste Fassung hat genau das getan, weil es bequem war. Ich habe das später korrigiert, und es ist einer der Punkte, die mir bei jedem späteren Sicherheitsdurchgang wieder begegnet sind.
Drittens: Was ist mit Anfragen von fremden Seiten? Eine Weboberfläche mit Cookie-Anmeldung ist grundsätzlich anfällig dafür, dass eine fremde Seite im Namen des angemeldeten Nutzers etwas auslöst. Dagegen gibt es Standardmittel — Herkunftsprüfung bei verändernden Anfragen, richtig gesetzte Cookie-Attribute. Ich habe das damals halb verstanden umgesetzt und Jahre später richtig, an zentraler Stelle statt in jeder Route.
Warnung
Der häufigste Fehler bei solchen Dashboards ist nicht ein exotischer Angriff, sondern eine simple Auslassung: Eine einzelne Route, die die Rechteprüfung vergisst. Alle anderen prüfen, diese eine nicht. Genau das ist mir Jahre später passiert und bei einem Durchgang durch den eigenen Code aufgefallen — nicht bei einem Angriff. Deshalb gehört so eine Prüfung an eine Stelle, die man nicht vergessen kann, und nicht in jede Route einzeln.
Die Oberfläche selbst
Der sichtbare Teil war der einfachste. Eine Serverauswahl mit Symbolen, eine Seitenleiste mit Bereichen, Formulare mit den Einstellungen.
Zwei Entscheidungen daraus haben lange gehalten:
Kanäle und Rollen als Auswahl, nie als Texteingabe. Ein Feld, in das man eine ID tippt, ist eine Fehlerquelle. Eine Auswahlliste, die der Server füllt, kann nur gültige Werte enthalten.
Speichern pro Bereich, nicht pro Feld. Ich hatte zuerst automatisches Speichern bei jeder Änderung. Das fühlt sich modern an und ist bei Einstellungen unangenehm, weil man nicht sieht, was gerade passiert ist, und weil ein halb ausgefülltes Formular Zwischenzustände in die Datenbank schreibt.
Wie die Änderung zum Bot kommt
Die Frage aus der Skizze — wie erfährt der Bot von einer Änderung — habe ich pragmatisch beantwortet: Er erfährt es nicht. Er schaut nach.
Jeder Servereintrag hat einen Zeitstempel der letzten Änderung. Der Bot hält seine Einstellungen in einem Zwischenspeicher und prüft beim Zugriff, ob der Stand noch aktuell ist. Bei einer Änderung ist der nächste Zugriff langsam, alle weiteren sind schnell.
Das ist nicht die eleganteste Lösung. Es gibt keine sofortige Wirkung, keine Ereignisse, keinen zusätzlichen Dienst. Aber es hat einen Vorteil, der über die Jahre schwerer wog als alles andere: Es kann nicht auseinanderlaufen. Es gibt keinen Zustand, in dem Web und Bot verschiedene Wahrheiten haben, weil eine Nachricht verloren ging.
Was ich beim Aufbau der Oberfläche gelernt habe
Der technische Teil war absehbar. Interessanter war, wie viele kleine Entscheidungen darüber bestimmen, ob eine Konfigurationsoberfläche benutzbar ist.
Speichern muss sichtbar sein. Meine erste Fassung speicherte automatisch bei jeder Änderung. Das fühlt sich modern an und erzeugt Unsicherheit: Ist das jetzt gespeichert? Kann ich das noch abbrechen? Ein ausdrücklicher Knopf mit Rückmeldung ist bei Einstellungen die bessere Wahl.
Der Ausgangszustand muss erkennbar sein. Ein leeres Feld kann bedeuten „nicht gesetzt" oder „konnte nicht geladen werden". Diese beiden Zustände sehen gleich aus und bedeuten das Gegenteil voneinander.
Fehler gehören ans Feld, nicht an den Seitenanfang. Eine Meldung oben, die sagt „Speichern fehlgeschlagen", zwingt zum Suchen. Eine Meldung unter dem betroffenen Feld beantwortet die Frage sofort.
Nicht alles gleichzeitig laden. Meine erste Fassung holte beim Öffnen sämtliche Einstellungen aller Bereiche. Das dauerte spürbar und war unnötig, weil man immer nur einen Bereich ansieht.
Die Entscheidung, die ich rückblickend am wichtigsten finde
Es gab die Überlegung, das Dashboard direkt mit dem Bot-Prozess zu verbinden — über eine interne Schnittstelle, damit Änderungen sofort wirken.
Ich habe mich dagegen entschieden, und zwar aus einem Grund, der damals nach Bequemlichkeit klang: Ich wollte nicht, dass eine Änderung im Web davon abhängt, ob der Bot gerade läuft.
Dass das die richtige Entscheidung war, hat sich über die Jahre mehrfach gezeigt. Der Bot ist abgestürzt, neu gestartet, umgezogen, in einen Container verschoben — und in keinem dieser Fälle war das Dashboard betroffen. Die Kopplung, die ich nicht gebaut habe, ist die, die nie kaputtgehen konnte.
Aus dieser Erfahrung ist die Regel geworden, die drei Jahre später den ganzen Aufbau bestimmt hat: Prozesse teilen sich Daten, nicht Aufrufe.
Was ich beim ersten Ausliefern gelernt habe
Die Anmeldung funktionierte lokal einwandfrei und online gar nicht. Ursache: Die Weiterleitungsadresse muss bei Discord exakt hinterlegt sein, inklusive Protokoll und Pfad. http://localhost:3000/api/auth/callback/discord und https://beispiel.de/api/auth/callback/discord sind zwei verschiedene Einträge.
Der zweite Stolperstein war die öffentliche Adresse. Hinter einem Reverse Proxy sieht die Anwendung eine interne Adresse und baut daraus Links, die nach außen nicht funktionieren. Es gibt dafür eine Konfiguration, und man findet sie erst, nachdem man eine Stunde verwirrt war.
Beides sind keine intellektuellen Herausforderungen. Aber sie sind der Grund, warum ich seitdem jede neue Anwendung so früh wie möglich einmal richtig ausliefere, auch wenn sie noch nichts kann. Die Umgebungsprobleme kommen sowieso — besser an einem leeren Projekt als kurz vor der Freischaltung.