- Extend `users_o` and `subusers_o` with `two_factor_enabled` and `two_factor_secret` properties. - Implement methods for managing 2FA (`isTwoFactorEnabled`, `setTwoFactorSecret`, `verify_2fa_code`) in authentication logic. - Add 2FA handling in login flows for both users and subusers, including token generation and validation. - Introduce `totp` class for TOTP-based authentication, including QR code generation and code verification. - Add test cases for 2FA functionality (`TwoFactorAuthTest.php`) and coverage for login scenarios with 2FA. - Update OpenAPI specifications to include 2FA flows (`auth/2fa/setup`, `auth/2fa/enable`, `auth/2fa/verify`, `auth/2fa/disable`).
120 lines
3.0 KiB
PHP
120 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use Exception;
|
|
|
|
class totp
|
|
{
|
|
private string $base32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
|
/**
|
|
* Generate a secret
|
|
* @param int $length
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function generateSecret(int $length = 16): string
|
|
{
|
|
$secret = '';
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$secret .= $this->base32_chars[random_int(0, 31)];
|
|
}
|
|
return $secret;
|
|
}
|
|
|
|
/**
|
|
* Get the TOTP code
|
|
* @param string $secret
|
|
* @param int|null $time
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function getCode(string $secret, int $time = null): string
|
|
{
|
|
if ($time === null) {
|
|
$time = floor(time() / 30);
|
|
}
|
|
|
|
$base32_secret = $this->base32Decode($secret);
|
|
|
|
// Pack time into binary string
|
|
$time_binary = pack('N*', 0) . pack('N*', $time);
|
|
|
|
// HMAC-SHA1
|
|
$hash = hash_hmac('sha1', $time_binary, $base32_secret, true);
|
|
|
|
// Dynamic truncation
|
|
$offset = ord($hash[19]) & 0xf;
|
|
$otp = (
|
|
((ord($hash[$offset + 0]) & 0x7f) << 24) |
|
|
((ord($hash[$offset + 1]) & 0xff) << 16) |
|
|
((ord($hash[$offset + 2]) & 0xff) << 8) |
|
|
(ord($hash[$offset + 3]) & 0xff)
|
|
) % 1000000;
|
|
|
|
return str_pad((string)$otp, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
/**
|
|
* Verify the TOTP code
|
|
* @param string $secret
|
|
* @param string $code
|
|
* @param int $discrepancy
|
|
* @return bool
|
|
* @throws Exception
|
|
*/
|
|
public function verifyCode(string $secret, string $code, int $discrepancy = 1): bool
|
|
{
|
|
$current_time = floor(time() / 30);
|
|
|
|
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
|
if ($this->getCode($secret, $current_time + $i) === $code) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Base32 decode
|
|
* @param string $base32
|
|
* @return string
|
|
*/
|
|
private function base32Decode(string $base32): string
|
|
{
|
|
$base32 = strtoupper($base32);
|
|
if (!preg_match('/^[A-Z2-7]+$/', $base32)) {
|
|
return '';
|
|
}
|
|
|
|
$binary = '';
|
|
foreach (str_split($base32) as $char) {
|
|
$binary .= str_pad(decbin(strpos($this->base32_chars, $char)), 5, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
$binary_chunks = str_split($binary, 8);
|
|
$result = '';
|
|
foreach ($binary_chunks as $chunk) {
|
|
if (strlen($chunk) === 8) {
|
|
$result .= chr(bindec($chunk));
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Generate a QR code URL
|
|
* @param string $secret
|
|
* @param string $name
|
|
* @param string $issuer
|
|
* @return string
|
|
*/
|
|
public function getQrCodeUrl(string $secret, string $name, string $issuer): string
|
|
{
|
|
return 'otpauth://totp/' . rawurlencode($issuer) . ':' . rawurlencode($name) . '?secret=' . $secret . '&issuer=' . rawurlencode($issuer);
|
|
}
|
|
}
|