Initial commit
This commit is contained in:
184
lib/Stores/EnrollmentStore.php
Normal file
184
lib/Stores/EnrollmentStore.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\AuthenticationProviderTotp\Stores;
|
||||
|
||||
use KTXC\Db\DataStore;
|
||||
|
||||
/**
|
||||
* Store for TOTP enrollments and recovery codes
|
||||
*
|
||||
* Stores TOTP secrets and enrollment state for users.
|
||||
*
|
||||
* Collection: totp_enrollments
|
||||
* Schema: {
|
||||
* tid: string, // Tenant identifier
|
||||
* uid: string, // User identifier
|
||||
* secret: string, // Base32 encoded TOTP secret
|
||||
* recovery_codes: array, // Array of recovery codes
|
||||
* used_recovery_codes: array, // Indices of used recovery codes
|
||||
* verified: bool, // Whether enrollment is verified
|
||||
* created_at: int, // Creation timestamp
|
||||
* verified_at: int|null // Verification timestamp
|
||||
* }
|
||||
*/
|
||||
class EnrollmentStore
|
||||
{
|
||||
protected const COLLECTION_NAME = 'totp_enrollments';
|
||||
|
||||
public function __construct(protected DataStore $store)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Get verified enrollment for a user
|
||||
*/
|
||||
public function getEnrollment(string $tenantId, string $userId): ?array
|
||||
{
|
||||
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'verified' => true,
|
||||
]);
|
||||
|
||||
return $entry ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending (unverified) enrollment
|
||||
*/
|
||||
public function getPendingEnrollment(string $tenantId, string $userId): ?array
|
||||
{
|
||||
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'verified' => false,
|
||||
]);
|
||||
|
||||
return $entry ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a pending enrollment (not yet verified)
|
||||
*/
|
||||
public function storePendingEnrollment(string $tenantId, string $userId, string $secret, array $recoveryCodes = []): void
|
||||
{
|
||||
$collection = $this->store->selectCollection(self::COLLECTION_NAME);
|
||||
|
||||
// Remove any existing pending enrollment
|
||||
$collection->deleteMany([
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'verified' => false,
|
||||
]);
|
||||
|
||||
$collection->insertOne([
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'secret' => $secret,
|
||||
'recovery_codes' => $recoveryCodes,
|
||||
'used_recovery_codes' => [],
|
||||
'verified' => false,
|
||||
'created_at' => time(),
|
||||
'verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm enrollment (mark as verified)
|
||||
*/
|
||||
public function confirmEnrollment(string $tenantId, string $userId): void
|
||||
{
|
||||
$this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
[
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'verified' => false,
|
||||
],
|
||||
[
|
||||
'$set' => [
|
||||
'verified' => true,
|
||||
'verified_at' => time(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove enrollment (for unenrollment)
|
||||
*/
|
||||
public function removeEnrollment(string $tenantId, string $userId): void
|
||||
{
|
||||
$this->store->selectCollection(self::COLLECTION_NAME)->deleteMany([
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unused recovery codes for a user
|
||||
*
|
||||
* @return array<string> Unused recovery codes
|
||||
*/
|
||||
public function getRecoveryCodes(string $tenantId, string $userId): array
|
||||
{
|
||||
$enrollment = $this->getEnrollment($tenantId, $userId);
|
||||
if (!$enrollment) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$codes = $enrollment['recovery_codes'] ?? [];
|
||||
$usedIndices = $enrollment['used_recovery_codes'] ?? [];
|
||||
|
||||
// Filter out used codes
|
||||
$result = [];
|
||||
foreach ($codes as $index => $code) {
|
||||
if (!in_array($index, $usedIndices, true)) {
|
||||
$result[] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a recovery code as used
|
||||
*/
|
||||
public function markRecoveryCodeUsed(string $tenantId, string $userId, int $codeIndex): void
|
||||
{
|
||||
$this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
[
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'verified' => true,
|
||||
],
|
||||
[
|
||||
'$push' => [
|
||||
'used_recovery_codes' => $codeIndex,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new recovery codes (replaces existing)
|
||||
*
|
||||
* @param array<string> $codes New recovery codes
|
||||
*/
|
||||
public function replaceRecoveryCodes(string $tenantId, string $userId, array $codes): void
|
||||
{
|
||||
$this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
[
|
||||
'tid' => $tenantId,
|
||||
'uid' => $userId,
|
||||
'verified' => true,
|
||||
],
|
||||
[
|
||||
'$set' => [
|
||||
'recovery_codes' => $codes,
|
||||
'used_recovery_codes' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user