mitmario.dev

Abmelden

Node.js Sandbox 3 Min Lesezeit 3 BeispieleLektion 5 von 7

Abmelden sieht nach der einfachsten Route des ganzen Abschnitts aus: Cookie weg, fertig. Genau deshalb steht sie hier als eigene Lektion, denn dieses „fertig” ist falsch, und der Fehler ist in freier Wildbahn häufig.

Zwei Schritte, und nur der zweite zählt

Beim Abmelden passieren zwei Dinge. Der Browser wirft seinen Zettel weg, und der Server verwirft die Sitzung. Nur das zweite ist das Abmelden.

Der Zettel ist weg, die Sitzung nicht
// Der Ausweis liegt beim Browser, die Sitzung beim Server. Wer nur den
// Ausweis wegwirft, hat den Server nicht gefragt.
const sitzungen = new Map([["ABC123", { nutzer: "anna@beispiel.de" }]]);

const oeffnet = (ausweis) => sitzungen.get(ausweis)?.nutzer ?? "nichts mehr";

// Anna meldet sich ab. Variante eins: nur das Cookie loeschen.
console.log("nur das Cookie geloescht");
console.log("  Annas Browser hat:   keinen Ausweis mehr");
console.log(`  eine Kopie oeffnet:  ${oeffnet("ABC123")}`);

// Variante zwei: die Sitzung auf dem Server verwerfen.
sitzungen.delete("ABC123");
console.log("");
console.log("Sitzung auf dem Server verworfen");
console.log("  Annas Browser hat:   keinen Ausweis mehr");
console.log(`  eine Kopie oeffnet:  ${oeffnet("ABC123")}`);

Der Grund ist derselbe wie in 13.4: Der Ausweis liegt beim Besucher, die Sitzung liegt bei dir. Was beim Besucher liegt, kannst du nicht zurückholen, du kannst ihn nur bitten, es wegzuwerfen. Wer den Wert vorher irgendwo abgeschrieben hat, aus einem geteilten Rechner, aus einem Protokoll, aus einer Browser-Erweiterung, der kommt damit weiter hinein. Und zwar so lange, bis die Sitzung von selbst abläuft, also unter Umständen Wochen.

Ein sitzungen.delete(...) ist eine Zeile. Sie ist der Unterschied zwischen „der Besucher sieht die Anmeldeseite” und „der Besucher ist abgemeldet”.

Nebenbei bekommst du damit eine Funktion geschenkt, nach der Nutzer öfter fragen: „Auf allen Geräten abmelden”. Wenn deine Sitzungen serverseitig liegen und wissen, zu wem sie gehören, ist das ein Durchlauf über die Sitzungen dieses Nutzers und ein delete je Treffer. Mit einem signierten Token ohne Serverzustand geht genau das nicht, und das ist der Hauptgrund, warum dieser Kurs die Sitzung nimmt.

Der erste Schritt hat trotzdem seine Tücke. Es gibt keinen Befehl „lösch das Cookie”, sondern nur noch eine Set-Cookie-Zeile, diesmal mit leerem Wert und einem Ablaufdatum in der Vergangenheit.

Löschen heißt, dieselbe Zeile noch einmal zu schreiben
import express from "express";

const app = express();

// Gesetzt wird das Cookie mit einem eigenen Pfad.
app.get("/setzen", (req, res) => {
  res.cookie("sid", "ABC123", { path: "/intern", httpOnly: true, sameSite: "lax" });
  res.end();
});

// So nicht: geloescht wird ohne diesen Pfad. Fuer den Browser sind das
// zwei verschiedene Cookies, das erste bleibt also stehen.
app.get("/loeschen-falsch", (req, res) => {
  res.clearCookie("sid");
  res.end();
});

// So: dieselben Attribute wie beim Setzen.
app.get("/loeschen-richtig", (req, res) => {
  res.clearCookie("sid", { path: "/intern", httpOnly: true, sameSite: "lax" });
  res.end();
});

app.listen(3000);

Ruf /setzen auf, dann /loeschen-falsch, dann wieder /setzen und /loeschen-richtig. Die Unterschiede stehen in der Kopfzeile, also im Terminal:

curl -i http://localhost:3000/loeschen-falsch

curl -i http://localhost:3000/loeschen-richtig

Der Browser ordnet ein Cookie nach Name, Domain und Pfad zu. Passt eines davon nicht, hält er die Löschanweisung für ein anderes Cookie, und das alte bleibt friedlich stehen. Deshalb gehören in clearCookie dieselben Angaben wie in cookie. In unserer Aufgabe ist der Pfad überall der Standard, sodass es reicht, an httpOnly und sameSite zu denken.

Abmelden ist ein POST

Bleibt die Frage nach der Methode. Abmelden verändert etwas, also gehört es nicht hinter ein GET. Das ist keine Formalie.

Ein Bild, das dich abmeldet
import express from "express";

const app = express();
const sitzungen = new Map([["ABC123", { nutzer: "anna@beispiel.de" }]]);
const stand = () => (sitzungen.has("ABC123") ? "steht noch" : "weg");

// So nicht: abmelden per GET. Jeder Aufruf der Adresse meldet ab, und
// ein Bild auf einer fremden Seite ist so ein Aufruf.
app.get("/abmelden-per-get", (req, res) => {
  sitzungen.delete("ABC123");
  res.type("text/plain").send("abgemeldet");
});

// So: abmelden nur per POST, und GET sagt ausdruecklich, dass es das
// nicht tut.
app.post("/logout", (req, res) => {
  sitzungen.delete("ABC123");
  res.type("text/plain").send("abgemeldet");
});
app.get("/logout", (req, res) => {
  res.status(405).set("Allow", "POST").type("text/plain").send("Zum Abmelden bitte das Formular benutzen.");
});

app.listen(3000);

Rechts steht die Abmeldung per GET, und sie funktioniert, obwohl niemand auf einen Knopf gedrückt hat. Genau das ist das Problem.

Ein GET löst jeder aus, der eine Adresse in eine Seite schreibt, und ein Bild-Element mit deiner Abmelde-Adresse als Quelle ist genau das. Der Browser lädt es, schickt Annas Cookie mit, und Anna ist weg. Für sich genommen ist der Schaden gering, sie ärgert sich und meldet sich neu an. Wichtig ist das Muster dahinter, denn dieselbe fremde Seite kann auch auf „löschen”, „bestellen” oder „überweisen” zeigen. Das hat einen Namen, CSRF, und SameSite aus 13.3 ist die Abwehr. In 15.4 kommt das ausführlich.

Ein Detail im Beispiel gehört zum guten Ton: Wer /logout per GET aufruft, bekommt keinen 404, sondern 405 Method Not Allowed und eine Allow-Kopfzeile, die sagt, was ginge. 404 hieße „gibt es nicht”, und das stimmt ja nicht.

Zum Mitnehmen

Das Cookie zu löschen fühlt sich nach Abmelden an und ist keins. Wer den Ausweis vorher kopiert hat, ist weiter drin. Zählen tut nur, was auf dem Server passiert.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.