Angemeldet bleiben
Benutzern die Möglichkeit zu geben angemeldet zu bleiben steigert deutlich die Benutzerfreundlichkeit eurer Anwendung. So ist beispielsweise kein ständiger neuer Login zu Facebook o.ä. nötig, selbst wenn der Browser mal geschlossen wird.
So eine Funktion birgt aber auch ein entsprechendes Sicherheitsrisiko, da auf dem Rechner des Benutzers gewisse Authentifizierungsinformationen abgespeichert werden müssen. Diese könnten von einem Angreifer kopiert werden und ausgenutzt werden. Für sehr kritische Anwendungen sollte man auf diese Funktion lieber verzichten.
Dieser Artikel erklärt euch, wie ihr diese Funktion mit maximaler Sicherheit implementiert. Weitere Sicherheitstipps für den internen Bereich findet ihr im Artikel Authentifizierung in PHP.
Das komplette Login-Script könnt ihr hier herunterladen.
Inhaltsverzeichnis
Genereller Ablauf
Der Ablauf unseres Scripts sieht wie folgt aus:
- Bietet beim Login die Checkbox 'Angemeldet bleiben' an. Hat der Benutzer diese ausgewählt wird ein zusätzlicher Login-Cookie bei ihm abgespeichert.
- Erzeugt zwei sichere Codes. Der erste Code ist der Identifier, der zweite der Securitytoken.
- Speichert in einer Tabelle die User-ID, den Identifier sowie den SHA-1 Wert des Securitytokens.
- Beim Benutzer selbst wird der Identifier und der Scuritytoken (als Klartext) als Cookie hinterlegt.
- Besucht der Nutzer eure Website erneut und ist noch nicht eingeloggt, so lest ihr aus dem Cookie den Identifier und den Securitytoken aus.
- Sollte der Identifier und Securitytoken zu den Daten in der Tabelle stimmen, so loggt ihr dem Benutzer ein und erzeugt einen erneuten Securitytoken (den Identifier könnt ihr wiederverwenden). Der neue Securitytoken wird im Cookie und in der Datenbank aktualisiert.
- Passt der Securitytoken nicht zu dem Token in der Datenbank, so hat jemand höchst wahrscheinlich die Cookies beim Benutzer gestohlen.
- Ist kein Identifier in den Cookies vorhanden oder passt ist keiner in der Datenbank vorhanden, dann könnt ihr den Wert ignorieren.
Datenbanktabelle
Zum Abspeichern des Identifiers und des Securitytokens erzeugt eine neue Tabelle securitytokens:
Der SQL-Code für diese Tabelle ist:
1 2 3 4 5 6 7 |
CREATE TABLE `securitytokens` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` int(10) NOT NULL, `identifier` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `securitytoken` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; |
Erweiterung des Login-Formulars
Das Login-Formular solltet ihr um eine Checkbox erweitern für die Möglichkeit angemeldet zu bleiben. Für das Login-Formular aus unserem Loginscript-Beispiel sähe dies wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 |
<form action="?login=1" method="post"> E-Mail:<br> <input type="email" size="40" maxlength="250" name="email"><br><br> Dein Passwort:<br> <input type="password" size="40" maxlength="250" name="passwort"><br> <label><input type="checkbox" name="angemeldet_bleiben" value="1"> Angemeldet bleiben</label><br> <input type="submit" value="Abschicken"> </form> |
Erweiterung des Login-Prozess
Den Login-Bereich müsst ihr wie folgt modifizieren:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?php session_start(); $pdo = new PDO('mysql:host=localhost;dbname=test', 'root', ''); function random_string() { if(function_exists('random_bytes')) { $bytes = random_bytes(16); $str = bin2hex($bytes); } else if(function_exists('openssl_random_pseudo_bytes')) { $bytes = openssl_random_pseudo_bytes(16); $str = bin2hex($bytes); } else if(function_exists('mcrypt_create_iv')) { $bytes = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); $str = bin2hex($bytes); } else { //Bitte euer_geheim_string durch einen zufälligen String mit >12 Zeichen austauschen $str = md5(uniqid('euer_geheimer_string', true)); } return $str; } if(isset($_GET['login'])) { $email = $_POST['email']; $passwort = $_POST['passwort']; $statement = $pdo->prepare("SELECT * FROM users WHERE email = :email"); $result = $statement->execute(array('email' => $email)); $user = $statement->fetch(); //Überprüfung des Passworts if ($user !== false && password_verify($passwort, $user['passwort'])) { $_SESSION['userid'] = $user['id']; //Möchte der Nutzer angemeldet beleiben? if(isset($_POST['angemeldet_bleiben'])) { $identifier = random_string(); $securitytoken = random_string(); $insert = $pdo->prepare("INSERT INTO securitytokens (user_id, identifier, securitytoken) VALUES (:user_id, :identifier, :securitytoken)"); $insert->execute(array('user_id' => $user['id'], 'identifier' => $identifier, 'securitytoken' => sha1($securitytoken))); setcookie("identifier",$identifier,time()+(3600*24*365)); //1 Jahr Gültigkeit setcookie("securitytoken",$securitytoken,time()+(3600*24*365)); //1 Jahr Gültigkeit } die('Login erfolgreich. Weiter zu <a href="geheim.php">internen Bereich</a>'); } else { $errorMessage = "E-Mail oder Passwort war ungültig<br>"; } } ?> |
Interner Bereich
Auf euren internen Seiten, die erst nach der Authentifizierung zugänglich sein sollen, baut ihr folgenden Script ein.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<?php session_start(); $pdo = new PDO('mysql:host=localhost;dbname=test', 'root', ''); function random_string() { if(function_exists('random_bytes')) { $bytes = random_bytes(16); $str = bin2hex($bytes); } else if(function_exists('openssl_random_pseudo_bytes')) { $bytes = openssl_random_pseudo_bytes(16); $str = bin2hex($bytes); } else if(function_exists('mcrypt_create_iv')) { $bytes = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); $str = bin2hex($bytes); } else { //Bitte euer_geheim_string durch einen zufälligen String mit >12 Zeichen austauschen $str = md5(uniqid('euer_geheimer_string', true)); } return $str; } //Überprüfe auf den 'Angemeldet bleiben'-Cookie if(!isset($_SESSION['userid']) && isset($_COOKIE['identifier']) && isset($_COOKIE['securitytoken'])) { $identifier = $_COOKIE['identifier']; $securitytoken = $_COOKIE['securitytoken']; $statement = $pdo->prepare("SELECT * FROM securitytokens WHERE identifier = ?"); $result = $statement->execute(array($identifier)); $securitytoken_row = $statement->fetch(); if(sha1($securitytoken) !== $securitytoken_row['securitytoken']) { die('Ein vermutlich gestohlener Security Token wurde identifiziert'); } else { //Token war korrekt //Setze neuen Token $neuer_securitytoken = random_string(); $insert = $pdo->prepare("UPDATE securitytokens SET securitytoken = :securitytoken WHERE identifier = :identifier"); $insert->execute(array('securitytoken' => sha1($neuer_securitytoken), 'identifier' => $identifier)); setcookie("identifier",$identifier,time()+(3600*24*365)); //1 Jahr Gültigkeit setcookie("securitytoken",$neuer_securitytoken,time()+(3600*24*365)); //1 Jahr Gültigkeit //Logge den Benutzer ein $_SESSION['userid'] = $securitytoken_row['user_id']; } } if(!isset($_SESSION['userid'])) { die('Bitte zuerst <a href="login.php">Einloggen</a>'); } $userid = $_SESSION['userid']; echo "Hallo User: ".$userid; ?> |
Dieser Script überprüft, ob der User nicht eingeloggt ist und ob die notwendigen Cookies gesetzt wurden. Danach wird der Identifier und der Securitytoken aus den Cookies ausgelesen und mit der Datenbank verglichen. Stimmen alle Eingaben überein, so wird ein neuer Securitytoken geniert, dieser in der Datenbank als auch im Cookie abgespeichert und die notwendige Session-Variable $_SESSION['userid'] wird registriert.
Erweiterung des Logout-Prozess
Bei dem Logout müsst ihr sicherstellen, dass die Cookies entsprechend gelöscht werden. Dazu müsst ihr die logout.php Datei wie folgt verändern:
1 2 3 4 5 6 7 8 9 10 |
<?php session_start(); session_destroy(); //Cookies entfernen setcookie("identifier","",time()-(3600*24*365)); setcookie("securitytoken","",time()-(3600*24*365)); echo "Logout erfolgreich"; ?> |
Hintergrundwissen zum Sicherheitskonzept
Leider findet immer noch viele Websites, die statt dieser Variante direkt das Passwort und den Benutzernamen in dem Cookie abspeichern. Dies führt gleich zu mehreren Sicherheitsrisiken. Oftmals sind Cookies im Browser nicht sonderlich gut geschützt und jeder mit Zugriff auf den Computer kann diese kinderleicht auslesen. Somit hat der Angreifer auch direkt die notwendigen Logindaten.
Das Abspeichern des Passworts als Hash, den ihr in der User-Tabelle gespeichert habt, erhöht die Sicherheit in diesem Aspekt etwas, öffnet aber ein weiteres Sicherheitsproblem: Falls jemand an den Passworthash kommt, beispielsweise mittels SQL-Injection, so kann er sich selbst diese Cookies setzen und sich damit als beliebiger Nutzer einloggen.
Diese Variante bietet Schutz gegen mehrere Angriffsszenarien:
- Auf dem Rechner werden keine sensiblen Daten gespeichert wie beispielsweise das Passwort in Klartext.
- Gestohlene Daten aus der Datenbank, z.B. per SQL-Injection, erlauben es dem Angreifer nicht sich per Angemeldet bleiben-Cookie sich zu authentifizieren. Nach dem Diebstahl hat er nur den SHA-1 Hash, er benötigt aber den zufälligen Wert der diesen SHA-1-Hash erzeugt, und sofern euer Zufallsgenerator sicher funktioniert hat der Angreifer keine Chance diesen Wert zu berechnen.
- Stiehlt ein Angreifer den Cookie von der Festplatte des Benutzers, dann kann er sich damit einloggen. Allerdings nur sofern der Securitytoken noch aktuell ist. Hat sich der Benutzer in der Zwischenzeit erneut eingeloggt, ist der gestohlene Securitytoken wertlos.