Files
api/services/nginx/app/classes/totp.php
T
Jeppe Bundgaard f6b526f4ef 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`).
2026-02-23 17:00:18 +01:00

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);
}
}