Initial commit
This commit is contained in:
127
lib/Provider.php
Normal file
127
lib/Provider.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user