Files
authentication_provider_totp/lib/Provider.php
2026-02-10 20:06:34 -05:00

128 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\AuthenticationProviderTotp;
use KTXF\Security\Authentication\AuthenticationProviderAbstract;
use KTXF\Security\Authentication\ProviderContext;
use KTXF\Security\Authentication\ProviderResult;
use KTXM\AuthenticationProviderTotp\Services\EnrollmentService;
/**
* TOTP Authentication Provider
*
* Implements Time-based One-Time Password (TOTP) according to RFC 6238.
* Compatible with FreeOTP, Google Authenticator, Authy, Microsoft Authenticator, etc.
*/
class Provider extends AuthenticationProviderAbstract
{
private const DEFAULT_DIGITS = 6;
public function __construct(
private readonly EnrollmentService $enrollmentService,
) {}
// =========================================================================
// Provider Implementation
// =========================================================================
public function type(): string
{
return 'authentication';
}
public function identifier(): string
{
return 'totp';
}
public function method(): string
{
return self::METHOD_CREDENTIAL;
}
public function label(): string
{
return 'TOTP Code';
}
public function description(): string
{
return 'Authenticate using Time-based One-Time Passwords (TOTP) from an authenticator app.';
}
public function icon(): string
{
return 'clock';
}
public function beginChallenge(ProviderContext $context): ProviderResult
{
$userIdentifier = $context->userIdentifier;
if (empty($userIdentifier)) {
return ProviderResult::failed(
ProviderResult::ERROR_INVALID_FACTOR,
'User identifier is required'
);
}
// Verify user is enrolled
if (!$this->enrollmentService->isEnrolled($userIdentifier)) {
return ProviderResult::failed(
ProviderResult::ERROR_NOT_ENROLLED,
'TOTP is not enrolled for this user'
);
}
// TOTP doesn't require sending anything to begin challenge
// The user generates the code on their device
return ProviderResult::challenge([
'type' => 'totp',
'message' => 'Enter the code from your authenticator app',
'digits' => self::DEFAULT_DIGITS,
]);
}
public function verifyChallenge(ProviderContext $context, string $code): ProviderResult
{
$userIdentifier = $context->userIdentifier;
if (empty($userIdentifier)) {
return ProviderResult::failed(
ProviderResult::ERROR_INVALID_FACTOR,
'User identifier is required'
);
}
$result = $this->enrollmentService->verifyCode($userIdentifier, $code);
if (!$result['success']) {
return ProviderResult::failed(
ProviderResult::ERROR_FACTOR_FAILED,
$result['error'] ?? 'Invalid verification code'
);
}
$identity = [
'user_id' => $userIdentifier,
'provider' => $this->identifier(),
];
if ($result['used_recovery_code'] ?? false) {
$identity['used_recovery_code'] = true;
}
return ProviderResult::success($identity);
}
/**
* TOTP can also be used as a credential method (verify directly without challenge)
*/
public function verify(ProviderContext $context, string $secret): ProviderResult
{
return $this->verifyChallenge($context, $secret);
}
}