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

205 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\AuthenticationProviderTotp\Controllers;
use KTXC\Http\Response\JsonResponse;
use KTXC\SessionIdentity;
use KTXF\Controller\ControllerAbstract;
use KTXF\Routing\Attributes\AuthenticatedRoute;
use KTXM\AuthenticationProviderTotp\Services\EnrollmentService;
/**
* TOTP Enrollment Controller
*
* Handles TOTP device enrollment for users:
* - Get enrollment status
* - Begin enrollment (generate QR code)
* - Complete enrollment (verify code)
* - Remove enrollment
* - Regenerate recovery codes
*/
class EnrollmentController extends ControllerAbstract
{
public function __construct(
private readonly SessionIdentity $userIdentity,
private readonly EnrollmentService $enrollmentService,
) {}
/**
* Get TOTP enrollment status for the current user
*/
#[AuthenticatedRoute('/totp/status', name: 'totp.status', methods: ['GET'])]
public function status(): JsonResponse
{
$userId = $this->userIdentity->identifier();
if (!$userId) {
return new JsonResponse(
['error' => 'User not authenticated', 'error_code' => 'unauthorized'],
JsonResponse::HTTP_UNAUTHORIZED
);
}
$isEnrolled = $this->enrollmentService->isEnrolled($userId);
return new JsonResponse([
'enrolled' => $isEnrolled,
'provider' => 'totp',
'label' => 'TOTP Code',
]);
}
/**
* Begin TOTP enrollment - generates secret and QR code
*/
#[AuthenticatedRoute('/totp/enroll', name: 'totp.enroll.begin', methods: ['POST'])]
public function beginEnrollment(string $accountName = ''): JsonResponse
{
$userId = $this->userIdentity->identifier();
if (!$userId) {
return new JsonResponse(
['error' => 'User not authenticated', 'error_code' => 'unauthorized'],
JsonResponse::HTTP_UNAUTHORIZED
);
}
$config = [];
if (!empty($accountName)) {
$config['account_name'] = $accountName;
}
$result = $this->enrollmentService->beginEnrollment($userId, $config);
if (!$result['success']) {
return new JsonResponse(
['error' => $result['error'], 'error_code' => $result['error_code']],
JsonResponse::HTTP_BAD_REQUEST
);
}
$enrollment = $result['enrollment'] ?? [];
return new JsonResponse([
'status' => 'pending',
'secret' => $enrollment['secret'] ?? '',
'provisioning_uri' => $enrollment['provisioning_uri'] ?? '',
'qr_code' => $enrollment['qr_code'] ?? '',
'recovery_codes' => $enrollment['recovery_codes'] ?? [],
]);
}
/**
* Complete TOTP enrollment - verify the initial code
*/
#[AuthenticatedRoute('/totp/enroll/verify', name: 'totp.enroll.verify', methods: ['POST'])]
public function completeEnrollment(string $code): JsonResponse
{
$userId = $this->userIdentity->identifier();
if (!$userId) {
return new JsonResponse(
['error' => 'User not authenticated', 'error_code' => 'unauthorized'],
JsonResponse::HTTP_UNAUTHORIZED
);
}
if (empty($code)) {
return new JsonResponse(
['error' => 'Verification code is required', 'error_code' => 'invalid_request'],
JsonResponse::HTTP_BAD_REQUEST
);
}
$result = $this->enrollmentService->completeEnrollment($userId, $code);
if (!$result['success']) {
return new JsonResponse(
['error' => $result['error'], 'error_code' => $result['error_code']],
JsonResponse::HTTP_BAD_REQUEST
);
}
return new JsonResponse([
'status' => 'enrolled',
'message' => 'TOTP successfully enabled for your account',
]);
}
/**
* Remove TOTP enrollment (disable 2FA)
*/
#[AuthenticatedRoute('/totp/enroll', name: 'totp.enroll.remove', methods: ['DELETE'])]
public function removeEnrollment(string $code): JsonResponse
{
$userId = $this->userIdentity->identifier();
if (!$userId) {
return new JsonResponse(
['error' => 'User not authenticated', 'error_code' => 'unauthorized'],
JsonResponse::HTTP_UNAUTHORIZED
);
}
// Verify current code before removing
$verifyResult = $this->enrollmentService->verifyCode($userId, $code);
if (!$verifyResult['success']) {
return new JsonResponse(
['error' => 'Invalid verification code', 'error_code' => 'invalid_code'],
JsonResponse::HTTP_BAD_REQUEST
);
}
$result = $this->enrollmentService->removeEnrollment($userId);
if (!$result['success']) {
return new JsonResponse(
['error' => $result['error'], 'error_code' => $result['error_code']],
JsonResponse::HTTP_BAD_REQUEST
);
}
return new JsonResponse([
'status' => 'removed',
'message' => 'TOTP has been disabled for your account',
]);
}
/**
* Admin endpoint: Get TOTP enrollment status for any user
*/
#[AuthenticatedRoute('/admin/status/{uid}', name: 'totp.admin.status', methods: ['GET'])]
public function adminStatus(string $uid): JsonResponse
{
// TODO: Add permission check for admin operations
$isEnrolled = $this->enrollmentService->isEnrolled($uid);
return new JsonResponse([
'enrolled' => $isEnrolled,
'provider' => 'totp',
]);
}
/**
* Admin endpoint: Remove TOTP enrollment for any user (no code verification required)
*/
#[AuthenticatedRoute('/admin/enrollment/{uid}', name: 'totp.admin.remove', methods: ['DELETE'])]
public function adminRemoveEnrollment(string $uid): JsonResponse
{
// TODO: Add permission check for admin operations
$result = $this->enrollmentService->removeEnrollment($uid);
if (!$result['success']) {
return new JsonResponse(
['error' => $result['error'], 'error_code' => $result['error_code']],
JsonResponse::HTTP_BAD_REQUEST
);
}
return new JsonResponse([
'status' => 'removed',
'message' => 'TOTP enrollment removed successfully',
]);
}
}