Files
2026-02-10 20:04:26 -05:00

155 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\AuthenticationProviderPassword;
use KTXF\Security\Authentication\AuthenticationProviderAbstract;
use KTXF\Security\Authentication\ProviderContext;
use KTXF\Security\Authentication\ProviderResult;
use KTXF\Security\Crypto;
use KTXM\AuthenticationProviderPassword\Stores\CredentialStore;
/**
* Password Authentication Provider
*
* Authenticates users against local database credentials.
*/
class Provider extends AuthenticationProviderAbstract
{
public function __construct(
private readonly CredentialStore $store,
private readonly Crypto $crypto,
) { }
// =========================================================================
// Provider Implementation
// =========================================================================
public function type(): string
{
return 'authentication';
}
public function identifier(): string
{
return 'password';
}
public function method(): string
{
return self::METHOD_CREDENTIAL;
}
public function label(): string
{
return 'Password';
}
public function description(): string
{
return 'Authenticate using username and password stored in the local database.';
}
public function icon(): string
{
return 'lock';
}
public function verify(ProviderContext $context, string $secret): ProviderResult
{
if (empty($context->tenantId)) {
return ProviderResult::failed(
ProviderResult::ERROR_INTERNAL,
'Invalid tenant context'
);
}
$identity = $context->userIdentity;
if (empty($identity)) {
return ProviderResult::failed(
ProviderResult::ERROR_INVALID_CREDENTIALS,
'Identity is required'
);
}
// Fetch credentials (timing-safe: always compute hash)
$storedCredential = $this->store->fetchByIdentifier($context->tenantId, $identity);
// Always verify password to prevent timing attacks
$dummyHash = '$2y$10$' . str_repeat('0', 53);
$hashToVerify = $storedCredential['secret'] ?? $dummyHash;
$isValid = $this->crypto->verifyPassword($secret, $hashToVerify);
if (!$storedCredential || !$isValid) {
return ProviderResult::failed(
ProviderResult::ERROR_INVALID_CREDENTIALS,
'Invalid credentials'
);
}
// Check if password needs rehash
if ($this->crypto->needsRehash($hashToVerify)) {
$newHash = $this->crypto->hashPassword($secret);
$this->store->updateSecret($context->tenantId, $identity, $newHash);
}
return ProviderResult::success([
'identity' => $identity,
'provider' => $this->identifier(),
]);
}
// =========================================================================
// Credential Management
// =========================================================================
/**
* Set or update user credentials
*/
public function setCredential(string $tenantId, string $userId, string $password): bool
{
if (empty($tenantId)) {
return false;
}
$hash = $this->crypto->hashPassword($password);
// Check if credential exists, then update or create
if ($this->store->exists($tenantId, $userId)) {
return $this->store->updateSecret($tenantId, $userId, $hash);
}
return $this->store->create($tenantId, $userId, $hash);
}
/**
* Verify a password without full authentication
*/
public function verifyPassword(string $tenantId, string $userId, string $password): bool
{
if (empty($tenantId)) {
return false;
}
$storedCredential = $this->store->fetchByIdentifier($tenantId, $userId);
if (!$storedCredential) {
return false;
}
return $this->crypto->verifyPassword($password, $storedCredential['secret']);
}
/**
* Check if user has credentials set
*/
public function hasCredentials(string $tenantId, string $userId): bool
{
if (empty($tenantId)) {
return false;
}
return $this->store->exists($tenantId, $userId);
}
}