Do svých instalací Admineru jsem si přidělal ověřování jednorázového hesla (OTP – One Time Password, které se dá použít pro Two-step authentication), zadaného např. pomocí Google Authenticatoru nebo podobné aplikace. Překvapilo mě, jak to bylo jednoduché a doporučoval bych tuto možnost dodělat do všech aplikací s přihlašováním heslem.
Pro vygenerování tajemství sdíleného mezi Google Authenticatorem a vaší aplikací stačí zavolat random_bytes(10)
. Pokud chceme možnost použití OTP dát všem uživatelům systému (a ne třeba jen jednomu administrátorovi), tak je vhodné každému vygenerovat vlastní tajemství a uložit ho do databáze.
Do aplikace se tento kód přenáší zakódovaný v Base32, k čemuž lze použít jednoduchou funkci:
<?php /** Zakódování řetězce do Base32 * @param string řetězec délky dělitelné pěti * @return string * @copyright Jakub Vrána, https://php.vrana.cz/ */ function base32_encode($data) { static $codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; $bits = ""; foreach (str_split($data) as $c) { $bits .= sprintf("%08b", ord($c)); } $return = ""; foreach (str_split($bits, 5) as $c) { $return .= $codes[bindec($c)]; } return $return; } ?>
Google Authenticator dovoluje informace i naskenovat pomocí QR kódu, k čemuž můžeme využít Google Charts API:
<?php /** Vygenerování URL s obrázkem QR kódu pro OTP * @param string název služby * @param string uživatelské jméno * @param string binární podoba tajemství * @return string URL * @copyright Jakub Vrána, https://php.vrana.cz/ */ function getOtpQrUrl($issuer, $user, $secret) { $otpAuth = "otpauth://totp/" . rawurlencode($issuer) . ":$user?secret=" . base32_encode($secret) . "&issuer=" . rawurlencode($issuer); return "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=" . urlencode($otpAuth); } ?>
Pokud informaci o tajemství nechcete předávat třetí straně, můžete QR kód vygenerovat lokálně, např. pomocí knihovny QR Code.
Při ověřování kódu zadaného uživatelem vygenerujeme ten stejný kód:
<?php /** Vygenerování jednorázového hesla * @param string binární podoba tajemství * @param string časový slot, typicky floor(time() / 30) * @return int * @copyright Jakub Vrána, https://php.vrana.cz/ */ function getOtp($secret, $timeSlot) { $data = str_pad(pack('N', $timeSlot), 8, "\0", STR_PAD_LEFT); $hash = hash_hmac('sha1', $data, $secret, true); $offset = ord(substr($hash, -1)) & 0xF; $unpacked = unpack('N', substr($hash, $offset, 4)); return ($unpacked[1] & 0x7FFFFFFF) % 1e6; } ?>
Takto vygenerovaný kód stačí porovnat s tím, co zadal uživatel, protože aplikace ho generuje stejně. Funkce pracuje s šesticifernými kódy, které jsou výchozí. Pokud ověření selže, tak bych doporučoval porovnat i kód pro předchozí (pro případ, že uživatel kód nestihl opsat včas) a následující (pokud se rozchází čas) $timeSlot
.
Diskuse je zrušena z důvodu spamu.