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); } }