Rolf B: Remember-Me-Funktion einrichten (CSRF-Token erstellen)

Beitrag lesen

Hallo borisbaer,

Kennst du da eine erste Anlaufstelle?

Naja, den Wikipedia-Artikel. Es gibt natürlich auch Libs, die das für Dich tun wollen (kannst Du selbst googlen), aber letztlich steckt nicht viel dahinter.

Du musst Dich auch nicht zwingend an JWT halten. Der Username, signiert mit einem SHA256 Key und base64-codiert, kann auch schon reichen.

Aber JWT wäre schick - wer Dich hacken will, kriegt einen Wow-Effekt und traut Dir gleich Fort-Knox Security zu 🤣.

Ich musste jetzt auch mal kurz lesen. Ein JWT ist entweder ein JWS (ein signiertes Token) oder ein JWE (ein verschlüsseltes Token). Für beide gibt's einen Internet-Standard (RfC): RFC7515 für JWS und RFC7516 für JWE. Wenn Du signieren UND verschlüsseln willst, sollst Du erst signieren und das Ergebnis in ein JWE-Token verpacken. Sagt RFC7519. Aber ich glaube, Dir reicht das Signieren 😀

JWS geht so: du baust zwei assoziative Arrays auf. Das eine, der Header, ist immer gleich, das andere, den Inhalt kannst Du frei gestalten, solange Du das JWS keinem übermitteln musst, der eine Norm einfordert. Aber Du kannst Dich ja an RFC7515 orientieren und iss (Issuer), sub (Subject), iat (Ausstellzeitpunkt) und exp (Expiration) belegen.

$header = [ "typ" => "JWT", "alg" => "HS256" ];
$payload = [ "iss" => "https://example.org",    // Deine URL
             "sub" => "borisbaer",              // User-ID für Autologon
             "iat" => 123456789,                // Unix-Timestamp der Ausstellung
             "exp" => 234567890 ];              // Unix-Timestamp für Ablauf

Die beiden Unix-Timestamps musst Du natürlich mit PHP korrekt ermitteln.

Die beiden Arrays musst Du jetzt - einzeln - als JSON-String formatieren, base64-codieren und danach nochmal URL-codieren (wegen des potenziellen = am Ende eines base64-Strings). Das Ganze hängst Du durch einen Punkt getrennt zusammen:

$code = urlencode(base64_encode(json_encode($header, JSON_FORCE_OBJECT)))
      . "." 
      . urlencode(base64_encode(json_encode($payload, JSON_FORCE_OBJECT)));

Diesen Code musst Du jetzt signieren. Dazu verwendest Du, weil "HS256" als Algorithmus angegeben ist, die hash_hmac-Funktion im PHP Deines Vertrauens und sagst ihr, sie soll "sha256" verwenden. Die zulässigen JSW-Algorithmen definiert übrigens RFC7518 und ich habe keine Ahnung, ob PHP die alle kennt. HMAC geht jedenfalls, und wenn Du mehr Bits nehmen willst, verwende den entsprechenden Code und den entsprechenden Parameter für hash_hmac.

Die Signatur codierst Du ebenfalls mit base64 und url_encode und hängst sie mit einem weiteren Punkt hintendran.

$signature = hash_hmac("sha256",
                       $code,
                       $aktueller_schluessel, // den musst Du irgendwo sicher ablegen
                       true);
$code .= "." . urlencode(base64_encode($signature));

Fertig. Das Ding steckst Du in einen Cookie. Der muss secure und HttpOnly sein, und deine Seite natürlich über https laufen.

Wenn Du das JWS nutzen willst, liest Du den Cookie-Inhalt aus und trennst alles in $header, $payload und $signature auf. Im Header schaust Du nach dem Signaturalgorithmus, dann signierst Du $header.".".$payload nochmal neu und vergleichst das mit der mitgelieferten Signatur. Wenn's passt, ist es dein Token und Du kannst die User-ID aus dem Inhaltsteil holen.

$tokenParts = explode(".", $jwsToken, 4);
if (count($tokenParts != 3) {
   // ungültiges Token
}
$header = json_decode(base64_decode(urldecode($tokenParts[0])), true);
// Ggf Header prüfen: Ists ein Array, ist typ=="JWT" und "alg"=="HS256"

$payload = json_decode(base64_decode(urldecode($tokenParts[1])), true); 
// Ist's ein Array, stimmt iss, ist exp noch nicht erreicht

// Jetzt den Schlüssel passend zum iat Zeitpunkt heraussuchen! Wenn Du exp ein
// Jahr nach iat setzt und bspw. 4x im Jahr den Schlüssel wechselt, brauchst Du
// eine Historie von 5 Schlüsseln. 

$tokensignature = base64_decode(urldecode($tokenParts[2]));
$realsignature = hash_hmac("sha256",
                           $tokenParts[0].".".$tokenParts[1],
                           $genutzter_schluessel,
                           true);

if (!hash_equals($tokensignature, $realsignature)) {
   // falsche Signatur
}

// Wenn der Schlüssel ein älterer, aber noch gültiger Schlüssel war, das
// Token neu signieren, damit es künftig mit dem aktuellen Schlüssel kommt

// Jetzt kann der Autologin erfolgen.
User::AutoLogin($payload["sub"]);

Das hat mir jetzt Spaß gemacht, das rauszusuchen. Ausprobiert habe ich es nicht, also viel Glück damit 😉. Und ich versichere Dir: damit gewinnst Du den Over-Engineering Orden dritter Klasse 🤣

Bin gespannt, welche Backpfeifen ich jetzt für mein Amateur-Gestümpere ernte. Bis vor einer Stunde wusste ich auch noch nicht, wie das alles geht.

Rolf

--
sumpsi - posui - obstruxi