Add two-factor authentication support for users and subusers
- 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`).
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user