PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


2-Faktor-Authentifizierung mit dem Google Authenticator

with 30 comments

Viele größere Webdienste bieten mittlerweile die 2-Faktor-Authentifizierung an, PayPal, Amazon, Facebook und nicht zuletzt Google. Mit der 2-Faktor-Authentifizierung muss neben dem Benutzernamen und Passwort auch noch ein Einmal-Passwort, ein sogenanntes One-Time-Token eingegeben werden, das von einem Gerät unabhängig vom PC generiert wird und nur einmal gültig ist. Sollte es also jemand schaffen und das Passwort erraten oder mitschneiden, hilft es dem Angreifer wenig denn er muss auch noch Zugriff auf das Hardwaregerät bekommen, und das sollte schwierig sein.

Die Generierung von solchen Codes ist nicht sonderlich schwer, beide Parteien (das Gerät und die Webseite) müssen nur ein gemeinsames „shared secret“ kennen (auch Seed genannt), und aufbauend auf diesem dann sich immer ändernde Codes generieren können. Dazu gibt es RFCs, beispielsweise RFC 4226.

Ich habe ja bereits letztes Jahr den Yubikey vorgestellt, heute möchte ich den Google Authenticator vorstellen und zeigen wie man seine eigene Webseite um diesen 2-Faktor-Login erweitern kann mittels PHP.

Der Google Authenticator ist als App für Handys konzipiert, man muss also keine Hardware kaufen sondern einfach nur ein App installieren, den Google Authenticator gibt es für Android, IPhone/iPad, Blackberry und weiteren Plattformen. Es gibt auch Umsetzungen für Windows, Java und Palm OS. Es ist Open Source.

Die Installation des Google Authenticators ist denkbar einfach und kann bei Google nachgelesen werden. Normalerweise geht das ganz einfach über den Market/AppStore.

Es gibt schon einige Umsetzungen in PHP da draußen, aber ich wollte meine eigene haben ;-). Und habe mir eine Klasse gebaut (GoogleAuthenticator auf Github) die alles kann was ich brauche:

  1. Ein neues Secret generieren
  2. Den aktuellen Code errechnen für ein gegebenes Secret
  3. Einen Code gegenchecken für ein gegebenes Secret (inklusive variabler Zeit-Abweichungs-Toleranz)
  4. Eine QR-Code-URL zurückgeben für ein QR-Code-Bild für ein gegebenes Secret
<?php

require_once '../PHPGangsta/GoogleAuthenticator.php';

$ga = new PHPGangsta_GoogleAuthenticator();

$secret = $ga->createSecret();
echo "Secret is: ".$secret."\n\n";

$qrCodeUrl = $ga->getQRCodeGoogleUrl('Blog', $secret);
echo "Google Charts URL for the QR-Code: ".$qrCodeUrl."\n\n";


$oneCode = $ga->getCode($secret);
echo "Checking Code '$oneCode' and Secret '$secret':\n";

$checkResult = $ga->verifyCode($secret, $oneCode, 2);    // 2 = 2*30sec clock tolerance
if ($checkResult) {
    echo 'OK';
} else {
    echo 'FAILED';
}

Die Webseite generiert nun also auf Anfrage ein Secret für einen Kunden, speichert dieses in einer Datenbank, und präsentiert dem Kunden dann entweder direkt das Secret das er dann abtippen muss, oder zeigen ihm den QR-Code der das Secret beinhaltet. Dieser QR-Code kann mit dem Google Authenticator bequem eingescannt werden, und schon werden auf dem Handy Codes angezeigt.

Die Webseite muss nun natürlich für einen aktivierten Benutzer noch ein weiteres Formular(feld) beim Login anzeigen in den ein gültiger Code einzugeben ist. Wenn dieser gültig war muss er unbedingt gespeichert werden und darf nicht erneut genutzt werden, denn sonst könnte ihn ein Mithörer in den nächsten Sekunden nutzen und sich damit auch anmelden. Nach einigen Minuten (je nach Einstellung der Toleranzgrenze) können diese alten benutzten Codes gelöscht werden.

Secret/Seed: OQB6ZZGYHCPSX4AK

Je nach Sicherheitslevel muss der Code bei jedem Login eingegeben werden, oder es wird ein Cookie gespeichert sodass nur alle X Tage der Code eingegeben werden muss. Auf einem neuen PC oder in einem anderen Browser wird der Code natürlich anfangs wieder benötigt. Je nachdem wie sicher bzw. bequem man es haben möchte.
Man kann das ganze optional anbieten, sodass nur Benutzer die es eingerichtet haben, nutzen können, oder verpflichtend für alle Benutzer einfordern. Auch hier ist das abhängig von der Sicherheitsstrategie der Seite/Firma.

Mögliche Fallstricke bzw. wichtige Dinge an die man denken sollte:

  1. Wichtig bei der Umsetzung des Google Authenticators ist die Zeitsynchronisierung. die Uhren von Server und Client dürfen nicht allzuweit auseinander liegen, sonst schlägt die AUthentifizierung immer fehl. Aus diesem Grund sollte man nicht nur die standardmäßig erlaubten 30 Sekunden Toleranz bieten sondern eventuell 1:30 oder 4:00 Minuten. Dazu berechnet der Server einfach die vorherigen und nächsten Codes und nimmt diese alle zum Vergleich.
  2. Wie oben bereits geschrieben müssen Replay-Attacken vermieden werden. Einmal benutzte Codes müssen also für den Benutzer auf eine Blacklist, sodass sie in den nächsten Minuten nicht nochmals verwendet werden können.
  3. Allgemein gilt natürlich auch hier ein Schutz gegen Brute-Force. Falls man 6 Zeichen Länge nimmt wie Google sind das nicht allzuviele Möglichkeiten die man durchprobieren müsste. Man muss also zwingend die Versuche pro Minute begrenzen.
  4. Falls das Handy verloren geht oder gestohlen wird, sollte man natürlich trotzdem noch eine Möglichkeit haben sich in den Account einzuloggen und das Secret zu widerrufen. Dazu kann man entweder ein zweites Mal auf einem Ersatzhandy den Google Authenticator einrichten, oder andere Wege wie Notfall-Codes festlegen, das Handy eines Freundes angeben um sich per SMS ein neues Passwort zuschicken zu lassen, oder oder.

Eine interessante Seite um etwas mit dem Secret, dem QR-Code und dem Code herumzuspielen ist diese Javascript-Umsetzung.

Für WordPress gibt es ein Plugin für die Google Authenticator Unterstützung, ich nehme an dass es für die (anderen) großen CMS etc. auch welche gibt.

Written by Michael Kliewe

März 13th, 2012 at 10:02 am

30 Responses to '2-Faktor-Authentifizierung mit dem Google Authenticator'

Subscribe to comments with RSS or TrackBack to '2-Faktor-Authentifizierung mit dem Google Authenticator'.

  1. Cool! Danke!
    Wie sieht das denn mit den Recovery-Codes aus, die es bei Google gibt? Die sind wohl einfach mit einem festen Zeitwert in getCode generiert?
    Also z.B. $c->getCode( $secret, 0 );

    darookee

    13 Mrz 12 at 11:10

  2. @darookee: Das könnte sein, feste Zeitwerte, oder aber eine extra Liste die für jeden Nutzer die Recovery-Codes enthält. Einfacher (weil man nichts speichern muss wenn alle den selben Zeitwert haben) sind jedoch die von dir genannten fest eingebauten Zeitpunkte. Gute Anregung, hab ich so noch gar nicht drüber nachgedacht 😉

    Michael Kliewe

    13 Mrz 12 at 22:29

  3. Das WordPress Plugin hatte ich mir vor einigen Wochen auch angeschaut.
    Ich fand aber die Idee Google für die Generierung des QR-Codes zu benutzen nicht schön und bin dann erstmal auf die Suche nach einer Lib für QR-Codes gegangen.
    Darüber ist der initiale Ansatz der Google Authenticators etwas eingeschlafen.

    Ansonsten finde ich es interessant wie bestimmte Themen unabhängig aber doch zeitgleich betrachtet werden :)

    Schöner Artikel btw.

    Norbert

    14 Mrz 12 at 15:42

  4. hey … sehr schöne sache, sowas hab ich schon seit längerem gesucht.

    dennoch steig ich noch nicht ganz dahinter, wo da die gewünschte sicherheit ist. ich verwende also auf meiner seite das oben genannte script, dieses erzeugt einen secret bzw. ein qrcode, das dem benutzer angezeigt wird. in der datenbank hinterlege ich zusätzlich den 6-stelligen token. mit diesem kann sich der benutzer also dann an meinem system anmelden.

    dem angreifer, reicht doch jetzt nach wie vor mein benutzername und passwort aus. den token kann er ja auch mit seinem handy durch google prüfen lassen.

    ich sehe im moment also keinerlei möglichkeit, diesen token auf mich zu personalisieren.

    rouven

    19 Mrz 12 at 12:45

  5. @rouven: Du speicherst in deiner Datenbank nur das Secret. Dieses muss dem Benutzer natürlich einmalig gesagt/gezeigt werden damit er seinen Google Authenticator konfigurieren kann. Man kann diesen natürlich auch per SMS an den Benutzer senden falls man davon ausgeht dass die Webseite unsicher ist und abgehört wird (weil kein https).
    Der Angreifer hat also dieses Secret nicht, und demnach kann er mit seinem Handy nichts tun, er weiß ja nicht wie er den Google Authenticator konfigurieren soll. Das ganze ist also nur sicher wenn der Angreifer das Secret nicht kennt, das muss man sicherstellen indem man dort auf jeden Fall https verwendet oder wie gesagt per Telefon/SMS/anderen Kanal das Secret übermittelt.
    Wenn das Secret ausgetauscht wurde kann ab dann auch gern ein unsicheres WLAN + http verwendet werden oder man sich einen Keylogger installieren lassen, ab dann darf der Angreifer gern zuhören. Aber den Secret-Austausch muss man natürlich sichern, sonst ist es ja kein Secret mehr 😉

    Michael Kliewe

    19 Mrz 12 at 13:11

  6. also zum verständnis:
    das script oben wird für den benutzer nur einmal ausgeführt, damit ich von google einen secret bekomme. dieses kann ich ihm direkt ausgeben, oder über den qrcode zeigen.
    ich dachte erst, das der qrcode bei jedem login angezeigt wird.

    hab ich jetzt den secret beim benutzer in der datenbank hinterlegt, frage ich zukünftig vom benutzer nur noch den 6-stelligen token ab.

    aber wie prüfe ich mit google jetzt die gültigkeit ab ??

    rouven

    19 Mrz 12 at 13:20

  7. @rouven: Den Secret bekommst du nicht von Google, den generiert das Script lokal, das ist einfach eine zufällige Zeichenreihenfolge. Der wird dann als Startpunkt für den Algorithmus verwendet.
    Der wird dem Benutzer nur einmal angezeigt damit er seinen Authenticator konfigurieren kann, korrekt. Und das muss eben auf einem sicheren Kanal passieren.
    Du fragst beim Benutzer dann im Login-Formular den aktuell gültigen Token ab den sein Authenticator gerade anzeigt (siehe Screenshot oben). Und du kannst nun auf Serverseite mit diesen Informationen (Secret aus der Datenbank + Token) die Funktion verifyCode() aufrufen und weißt dann ob der Token korrekt ist oder nicht.

    Michael Kliewe

    19 Mrz 12 at 13:36

  8. Ahhhh, es werde Licht :-)

    Vielen Dank !!!

    rouven

    19 Mrz 12 at 13:43

  9. Du vergewaltigst nicht PHP, sondern andere Projekte!

    Geklaut von:
    http://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/

    Ich würde mich schämen, andere Projekte als meine Arbeit zu publizieren!

    Frank Neff

    20 Mrz 12 at 14:41

  10. @Frank Kannst du das irgendwie etwas präzisieren? Ich sehe da keine Parallelen, außer dass wir beide eine Google Authenticator Umsetzung gebaut haben. Ich kenne den Artikel, den habe ich bei den Recherchen auch gefunden, aber ich behaupte nirgends dass das mein Projekt sei?! Was genau habe ich falsch gemacht?

    Das einzige was ich nicht selbst erstellt habe sind die base32-Decode und -Encode-Funktionen, das Rad neu zu erfinden war da nicht nötig. Aber auch die habe ich noch angepasst da sie einen Bug hatte und führende Nullen nicht berücksichtigt wurden.

    Ich schreibe ja oben dass ich nicht der erste bin und es bereits viele Lösungen gibt. Hier noch 2 weitere Umsetzungen die ich aber beide nicht ideal finde:
    http://blog.liip.ch/archive/2011/08/29/2-step-verification-with-google-authenticator-and-php.html
    http://code.google.com/p/ga4php/

    Wo genau habe ich das andere Projekt als meine Arbeit publiziert?

    Michael Kliewe

    20 Mrz 12 at 15:19

  11. Es gibt leider sehr viele Leute, welche Projekte herunterladen, Kommentare, Formatierung und ein paar Namen ändern, und das ganze als ihre eigene Arbeit publizieren (auch „Guttenberg Development“ genannt). Sollte ich mich irren, entschuldige & lösche bitte meinen evtl. etwas zu forschen Kommentar.

    Die Paralellen zwischen den zwei Klassen sind jedoch verblüffend… Ich entwickle zur Zeit selbst ein ähnliches Projekt, in meinem Header steht jedoch „Based on the solution of Phil, idontplaydarts.com“.

    Ich finde es einfach nicht fair, wesentliche Teile eines Softwareprojekts zu kopieren, aber diese nicht mit einem Wort zu erwähnen. Sollte ich dir Unrecht getan haben, bitte ich um Entschuldigung.

    Frank Neff

    20 Mrz 12 at 15:50

  12. @Frank „Die Paralellen zwischen den zwei Klassen sind jedoch verblüffend“
    Wo genau gibt es da Ähnlichkeiten? Ich muss Tomaten auf den Augen haben, aber die sind so unterschiedlich wie sie nur sein könnten.

    Wenn du die beiden Klassen nebeneinander legst und die jeweiligen Methoden miteinander vergleichst, wo siehst du da verblüffende Parallelen?
    http://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt
    https://github.com/PHPGangsta/GoogleAuthenticator/blob/master/PHPGangsta/GoogleAuthenticator.php

    Natürlich ist das Ergebnis der jeweiligen Methoden gleich, beide Klassen sollen beim base32-decoden das selbe errechnen, und auch verifyCode() bzw. getCode() funktionieren ähnlich da sie der Spezifikation entsprechen müssen. Aber wenn ich die Methodenrümpfe vergleiche sind die doch ziemlich unterschiedlich.

    Ich mag ja Kritik, aber sie muss auch Hand und Fuss haben. Du kannst dich jedenfalls beruhigt zurücklehnen, ich habe nichts von dort geklaut und möchte auch nicht den Code von anderen als meinen verkaufen, die Klasse habe ich nach bestem Wissen und Gewissen selbst erstellt und nur das base32 Zeug aus den PHP Manual Comments übernommen + etwas verbessert.

    Michael Kliewe

    20 Mrz 12 at 19:50

  13. Nadann kann ich mich nur entschuldigen. Sollte das nächste mal wohl besser nachschauen, bevor ich nen Shitstorm eröffne. Hoffe nimmst mir das nicht allzu übel…

    Frank Neff

    21 Mrz 12 at 14:26

  14. @Frank Hast es jedenfalls geschafft meinen Puls zu erhöhen 😉 Vergeben, vergessen, weiter kommentieren!

    PS: Wird deine Umsetzung Open Source irgendwo zu finden sein?

    Michael Kliewe

    21 Mrz 12 at 15:11

  15. @Michael: Danke dir 😉
    Ja meine Implementierung wird ne Extension für eZPublish, welche ich auf github und ez.no veröffentlichen werde. Jedoch ists noch ein weiter Weg zum ersten Major 😉

    Frank Neff

    21 Mrz 12 at 15:35

  16. ich nochmal :-)

    ich hab den code jetzt soweit in meine page eingebaut, funktioniert auch alles wunderbar.

    wenn ich aber jetzt über die page codes erzeugen möchte, kann ich mir ja auch den link zum qrcode-image ausgeben lassen ($ga->getQRCodeGoogleUrl(‚Blog‘, $secret);) evtl. im php mit urldecode umwandeln ??

    setz ich diese variabel jetzt im html in ein img src=““ zeigt der browser nur das broken-image symbol an, als könne er es nicht laden.

    gebe ich die ausgegebene url in die adresszeile des browsers ein, klappt es.

    wo liegt mein fehler ???

    rouven

    30 Okt 12 at 09:41

  17. https://github.com/PHPGangsta/GoogleAuthenticator

    PHPGangsta » 3 months ago » Fixed bug that causes the URL of the QR code to malform [edwardmp]

    Habs selbst hinbekommen. Hatte noch die alte Datei 😉

    rouven

    30 Okt 12 at 13:38

  18. Ich habe mein Code vergessen mit dem Rücksendecode komme ich wenigstens auf mein Dashboard aber ich möchte ihn jetzt wieder deinstallieren damit ich auch wieder vom Handy auf mein Wordpess zugreiffen kann wie mache ich das wenn ich mein Code nicht mehr weiß. Ich verzweifle noch.

    luisawgnr

    19 Jan 14 at 10:25

  19. @luisawgnr Ich kenne das WordPress-Plugin leider nicht genau. Da bleibt dir wahrscheinlich nur der Weg über die Datenbank, sprich direkt in der Datenbank die entsprechenden Einträge löschen. Wie genau die heißen und wo die sind kann ich auf Anhieb leider nicht sagen.

    Michael Kliewe

    20 Jan 14 at 01:51

  20. Das Prinzip ist mir klar: beim Erstaufruf ein QR-Code anzeigen und scannen lassen. Gleichzeitig Secret in der DB zum User speichern.

    Beim eigentlichen Login dann den eingegebenen PIN mittels verifyCode() prüfen.

    So weit in Ordnung.

    Wenn aber jetzt jemand irgendwie an die DB kommt, hat er das Secret ja in Klarschrift und kann sich seinen eigenen PIN erzeugen oder?

    $user_secret_ga = ‚LP22C3JMYTNOIFMZ‘;
    echo $ga->getCode($user_secret_ga );

    Irgendeine Idee wie man das Secret ver-hashen kann?

    DwB

    29 Apr 14 at 16:01

  21. Naja, hashen wird so nicht gehen, da du den Klartext für die Berechnung des Codes brauchst.

    Spontan fällt mir ein, diesen Eintrag zu verschlüsseln, z.B. mittels AES. Als Key kann man entweder einen globalen Key nehmen, der Hard-Coded ist oder sich ein Key aus den Userdaten zusammenbauen (z.B. Username + Anmeldedatum); man würde dann nach jedem Userlogin das Scret entschlüsseln.

    Ist alles nicht optimal, aber immerhin besser, als wenn das Secret als Plaintext in der Datenbank schlummert.

    TiTo

    18 Sep 14 at 12:01

  22. […] Beispiel verwende ich die von Michael Kliewe. Die Nutzung ist äußerst einfach (Listing 2, nach diesem Beispiel). Auf eine weitere Beschreibung verzichte ich, besser als die von Michael Kliewe könnte sie […]

  23. Danke für das Beispiel, das gefällt mir prinzipiell gut, ich finde aber die Art, wie der QR Code erzeugt wird, bedenklich – nämlich das Secret als URL-Parameter an Google zu übergeben.

    Damit ist es nicht nur samt Referer in deren Logfile, sondern gegebenenfalls auch in meiner Browser History.

    Ich habe das ein wenig sicherer gelöst, ich verwende für das Erzeugen des QR Codes eine lokale Library (zu finden auf http://phpqrcode.sourceforge.net/ ) – die kann in der aktuellen Version auch SVG Images erzeugen. D.h. das Bild ist im HTML-Sourcecode enthalten, es gibt keine Bilddatei, die dann irgendwo in einem Browsercache rumliegt, und keine Stelle, an der das Secret via http übergeben werden muss.

    Mein Code-Schnipsel:

    /* ------------ */
    require_once("phpqrcode/qrlib.php");

    function getQRCodeSVGImage($name, $secret, $title = null)
    {
    $qrcontent = 'otpauth://totp/'.$name.'?secret='.$secret;
    if(isset($title)) $qrcontent.= '&issuer='.urlencode($title);
    return QRcode::svg($qrcontent,'my-qr-code',false,QR_ECLEVEL_L,200);
    }
    /* ------------ */

    Martin

    3 Apr 15 at 10:04

  24. Hi!

    Ich versuche jetzt seit Stunden die von der Google-App erzeugten Codes zu verifizieren – ohne Erfolg. Zeit ist synchronisiert und den Zeitparameter habe ich schon verändert (steht jetzt auf 8). Gibt es noch eine Idee die ich probieren kann? Den QR-Code lasse ich ebenfalls via eigenem Script generieren.

    lga

    Andreas

    1 Jul 15 at 00:42

  25. … funktioniert nun. Hatte ein Leerzeichen im Secret drin. Habe ich vor Müdigkeit übersehen 😐

    Andreas

    1 Jul 15 at 01:38

  26. Also ich habe jetzt den Secret mit createSecret() generiert und gespeichert.

    Ich habe die Check-Routine aus dem Beispiel übernommen und als oneCode den Code aus der Authentificator-App genommen, bekomme aber nur FAILED.

    $secret = ‚5ZCC7C2RVRPYHK2K‘;
    $oneCode = 560442;
    $checkResult = $ga->verifyCode($secret, $oneCode, 6); // 2 = 2*30sec clock tolerance
    if ($checkResult)
    {
    echo ‚OK‘;
    }
    else
    {
    echo ‚FAILED‘;
    }

    Ist meine Überlegung falsch? Ist $oneCode irgendwas anderes?

    Danke!

    Fynn

    8 Aug 15 at 18:36

  27. Ich habe den Fehler gefunden. Die verifyCode-Funktion überprüft ob der Secret 6 Zeichen hat, aber der Secret den das Script generiert, ist viel länger. Was hat es damit auf sich?

    Fynn

    8 Aug 15 at 20:08

  28. […] 2-Faktor-Authentifizierung mit dem Google Authenticator […]

  29. Are there any reasons, why I should not use Google Authenticator as the single login method?

    Why do people tend to use GA as the second factor of a two factor authentication?

    Using GA as the single login method has the strong advantages, that users don’t need to remember their password any more. These day, users own a smartphone anyway.

    Stef

    11 Apr 16 at 09:28

  30. @Stef: Most of the time, the email address is the username, which is known to an attacker, or a thief who steals your phone.

    Second Factor means the login is based on „something you know in your head“, and „something you own“. Digital hackers can only steal the password, real thiefs on the street can own steal your phone. Stealing both is harder.

    If your username is secret, then that’s the first factor, GA the second, so you would not need a password. But it’s unusual.

    Michael Kliewe

    11 Apr 16 at 12:24

Leave a Reply

You can add images to your comment by clicking here.