From 576aab8af17326bac84e791db68650f9f61b6c3b Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Sun, 19 Jul 2026 18:36:45 -0400 Subject: [PATCH] Fix: use users profile mail address Signed-off-by: Sebastian Krupinski --- lib/Provider.php | 89 ++++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/lib/Provider.php b/lib/Provider.php index 4fb88bc..42bb801 100644 --- a/lib/Provider.php +++ b/lib/Provider.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\AuthenticationProviderMail; +use KTXC\Service\UserAccountsService; use KTXF\Mail\Object\Address; use KTXF\Mail\Object\MessagePropertiesBaseInterface; use KTXF\Mail\Provider\ProviderBaseInterface; @@ -17,9 +18,10 @@ use Psr\Log\LoggerInterface; /** * Email Challenge Authentication Provider - * - * Authenticates users by sending a one-time verification code to their email. - * Uses the mail manager system for sending verification emails. + * + * Authenticates users by sending a one-time verification code to the email + * address on the user's profile. The login identity is only used to resolve + * the user account, never as a mail recipient. */ class Provider extends AuthenticationProviderAbstract { @@ -29,6 +31,7 @@ class Provider extends AuthenticationProviderAbstract public function __construct( private readonly ChallengeStore $challengeStore, private readonly MailManager $mailManager, + private readonly UserAccountsService $userService, private readonly LoggerInterface $logger, ) {} @@ -68,14 +71,14 @@ class Provider extends AuthenticationProviderAbstract /** * Begin the email challenge - * - * Generates a verification code and sends it to the user's email. + * + * Generates a verification code and sends it to the email address on the + * user's profile. */ public function beginChallenge(ProviderContext $context): ProviderResult { $tenantId = $context->tenantId; - $userIdentity = $context->userIdentity; // Email address - + if (empty($tenantId)) { return ProviderResult::failed( ProviderResult::ERROR_INTERNAL, @@ -83,23 +86,16 @@ class Provider extends AuthenticationProviderAbstract ); } - if (empty($userIdentity)) { + $recipientAddress = $this->resolveRecipientAddress($context); + if ($recipientAddress === null) { return ProviderResult::failed( ProviderResult::ERROR_INVALID_CREDENTIALS, - 'Email address is required' - ); - } - - // Validate email format - if (!filter_var($userIdentity, FILTER_VALIDATE_EMAIL)) { - return ProviderResult::failed( - ProviderResult::ERROR_INVALID_CREDENTIALS, - 'Invalid email address format' + 'No valid email address on file for this account' ); } // Check for existing pending challenge (rate limiting) - if ($this->challengeStore->hasPending($tenantId, $userIdentity)) { + if ($this->challengeStore->hasPending($tenantId, $recipientAddress)) { // Allow resend but inform user $this->logger->debug('Resending email challenge', [ 'tenantId' => $tenantId, @@ -108,12 +104,12 @@ class Provider extends AuthenticationProviderAbstract // Generate and store challenge $ttl = $context->getConfig('code_ttl', self::DEFAULT_CODE_TTL); - $challenge = $this->challengeStore->create($tenantId, $userIdentity, $ttl); + $challenge = $this->challengeStore->create($tenantId, $recipientAddress, $ttl); // Send verification email $emailResult = $this->sendVerificationEmail( $tenantId, - $userIdentity, + $recipientAddress, $challenge['code'], $challenge['expires'], $context @@ -121,13 +117,13 @@ class Provider extends AuthenticationProviderAbstract if (!$emailResult['success']) { // Invalidate the challenge since email failed - $this->challengeStore->invalidate($tenantId, $userIdentity); - + $this->challengeStore->invalidate($tenantId, $recipientAddress); + $this->logger->error('Failed to send verification email', [ 'tenantId' => $tenantId, 'error' => $emailResult['error'] ?? 'Unknown error', ]); - + return ProviderResult::failed( ProviderResult::ERROR_INTERNAL, 'Failed to send verification email' @@ -145,10 +141,10 @@ class Provider extends AuthenticationProviderAbstract 'message' => 'A verification code has been sent to your email address', 'digits' => self::DEFAULT_DIGITS, 'expires_in' => $ttl, - 'masked_email' => $this->maskEmail($userIdentity), + 'masked_email' => $this->maskEmail($recipientAddress), ], [ - 'identity' => $userIdentity, + 'identity' => $recipientAddress, 'challenge_expires' => $challenge['expires'], ] ); @@ -160,8 +156,9 @@ class Provider extends AuthenticationProviderAbstract public function verifyChallenge(ProviderContext $context, string $code): ProviderResult { $tenantId = $context->tenantId; - $userIdentity = $context->userIdentity ?? $context->getMeta('identity'); - + // The address the code was sent to, stored when the challenge began + $recipientAddress = $context->getMeta('identity') ?? $this->resolveRecipientAddress($context); + if (empty($tenantId)) { return ProviderResult::failed( ProviderResult::ERROR_INTERNAL, @@ -169,10 +166,10 @@ class Provider extends AuthenticationProviderAbstract ); } - if (empty($userIdentity)) { + if (empty($recipientAddress)) { return ProviderResult::failed( ProviderResult::ERROR_INVALID_CREDENTIALS, - 'Identity is required' + 'No valid email address on file for this account' ); } @@ -180,14 +177,14 @@ class Provider extends AuthenticationProviderAbstract $code = preg_replace('/[\s\-]/', '', $code); // Verify the challenge - $result = $this->challengeStore->verify($tenantId, $userIdentity, $code); + $result = $this->challengeStore->verify($tenantId, $recipientAddress, $code); if (!$result['success']) { $this->logger->debug('Email challenge verification failed', [ 'tenantId' => $tenantId, 'error' => $result['error'] ?? 'Unknown', ]); - + return ProviderResult::failed( ProviderResult::ERROR_FACTOR_FAILED, $result['error'] ?? 'Invalid verification code' @@ -199,9 +196,9 @@ class Provider extends AuthenticationProviderAbstract ]); return ProviderResult::success([ - 'identity' => $userIdentity, + 'identity' => $context->userIdentity ?? $recipientAddress, 'provider' => $this->identifier(), - 'verified_email' => $userIdentity, + 'verified_email' => $recipientAddress, ]); } @@ -213,6 +210,32 @@ class Provider extends AuthenticationProviderAbstract return $this->verifyChallenge($context, $secret); } + private function resolveRecipientAddress(ProviderContext $context): ?string + { + $userIdentifier = $context->userIdentifier; + + if ($userIdentifier === null && !empty($context->userIdentity)) { + $user = $this->userService->fetchByIdentityRaw($context->userIdentity); + $userIdentifier = $user['uid'] ?? null; + } + + if ($userIdentifier === null) { + return null; + } + + $profile = $this->userService->fetchProfile($userIdentifier); + $address = trim((string)($profile['profile']['email'] ?? '')); + + if ($address === '' || !filter_var($address, FILTER_VALIDATE_EMAIL)) { + $this->logger->debug('No valid profile email address for email challenge', [ + 'tenantId' => $context->tenantId, + ]); + return null; + } + + return $address; + } + // ========================================================================= // Email Sending // =========================================================================