Initial commit

This commit is contained in:
root
2025-12-21 10:00:51 -05:00
committed by Sebastian Krupinski
commit b3d456d453
17 changed files with 3693 additions and 0 deletions

127
lib/Provider.php Normal file
View File

@@ -0,0 +1,127 @@
<?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);
}
}