Fix: use users profile mail address

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-19 18:36:45 -04:00
parent c451283863
commit 576aab8af1
+56 -33
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace KTXM\AuthenticationProviderMail; namespace KTXM\AuthenticationProviderMail;
use KTXC\Service\UserAccountsService;
use KTXF\Mail\Object\Address; use KTXF\Mail\Object\Address;
use KTXF\Mail\Object\MessagePropertiesBaseInterface; use KTXF\Mail\Object\MessagePropertiesBaseInterface;
use KTXF\Mail\Provider\ProviderBaseInterface; use KTXF\Mail\Provider\ProviderBaseInterface;
@@ -17,9 +18,10 @@ use Psr\Log\LoggerInterface;
/** /**
* Email Challenge Authentication Provider * Email Challenge Authentication Provider
* *
* Authenticates users by sending a one-time verification code to their email. * Authenticates users by sending a one-time verification code to the email
* Uses the mail manager system for sending verification emails. * 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 class Provider extends AuthenticationProviderAbstract
{ {
@@ -29,6 +31,7 @@ class Provider extends AuthenticationProviderAbstract
public function __construct( public function __construct(
private readonly ChallengeStore $challengeStore, private readonly ChallengeStore $challengeStore,
private readonly MailManager $mailManager, private readonly MailManager $mailManager,
private readonly UserAccountsService $userService,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
) {} ) {}
@@ -68,14 +71,14 @@ class Provider extends AuthenticationProviderAbstract
/** /**
* Begin the email challenge * 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 public function beginChallenge(ProviderContext $context): ProviderResult
{ {
$tenantId = $context->tenantId; $tenantId = $context->tenantId;
$userIdentity = $context->userIdentity; // Email address
if (empty($tenantId)) { if (empty($tenantId)) {
return ProviderResult::failed( return ProviderResult::failed(
ProviderResult::ERROR_INTERNAL, ProviderResult::ERROR_INTERNAL,
@@ -83,23 +86,16 @@ class Provider extends AuthenticationProviderAbstract
); );
} }
if (empty($userIdentity)) { $recipientAddress = $this->resolveRecipientAddress($context);
if ($recipientAddress === null) {
return ProviderResult::failed( return ProviderResult::failed(
ProviderResult::ERROR_INVALID_CREDENTIALS, ProviderResult::ERROR_INVALID_CREDENTIALS,
'Email address is required' 'No valid email address on file for this account'
);
}
// Validate email format
if (!filter_var($userIdentity, FILTER_VALIDATE_EMAIL)) {
return ProviderResult::failed(
ProviderResult::ERROR_INVALID_CREDENTIALS,
'Invalid email address format'
); );
} }
// Check for existing pending challenge (rate limiting) // Check for existing pending challenge (rate limiting)
if ($this->challengeStore->hasPending($tenantId, $userIdentity)) { if ($this->challengeStore->hasPending($tenantId, $recipientAddress)) {
// Allow resend but inform user // Allow resend but inform user
$this->logger->debug('Resending email challenge', [ $this->logger->debug('Resending email challenge', [
'tenantId' => $tenantId, 'tenantId' => $tenantId,
@@ -108,12 +104,12 @@ class Provider extends AuthenticationProviderAbstract
// Generate and store challenge // Generate and store challenge
$ttl = $context->getConfig('code_ttl', self::DEFAULT_CODE_TTL); $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 // Send verification email
$emailResult = $this->sendVerificationEmail( $emailResult = $this->sendVerificationEmail(
$tenantId, $tenantId,
$userIdentity, $recipientAddress,
$challenge['code'], $challenge['code'],
$challenge['expires'], $challenge['expires'],
$context $context
@@ -121,13 +117,13 @@ class Provider extends AuthenticationProviderAbstract
if (!$emailResult['success']) { if (!$emailResult['success']) {
// Invalidate the challenge since email failed // Invalidate the challenge since email failed
$this->challengeStore->invalidate($tenantId, $userIdentity); $this->challengeStore->invalidate($tenantId, $recipientAddress);
$this->logger->error('Failed to send verification email', [ $this->logger->error('Failed to send verification email', [
'tenantId' => $tenantId, 'tenantId' => $tenantId,
'error' => $emailResult['error'] ?? 'Unknown error', 'error' => $emailResult['error'] ?? 'Unknown error',
]); ]);
return ProviderResult::failed( return ProviderResult::failed(
ProviderResult::ERROR_INTERNAL, ProviderResult::ERROR_INTERNAL,
'Failed to send verification email' 'Failed to send verification email'
@@ -145,10 +141,10 @@ class Provider extends AuthenticationProviderAbstract
'message' => 'A verification code has been sent to your email address', 'message' => 'A verification code has been sent to your email address',
'digits' => self::DEFAULT_DIGITS, 'digits' => self::DEFAULT_DIGITS,
'expires_in' => $ttl, 'expires_in' => $ttl,
'masked_email' => $this->maskEmail($userIdentity), 'masked_email' => $this->maskEmail($recipientAddress),
], ],
[ [
'identity' => $userIdentity, 'identity' => $recipientAddress,
'challenge_expires' => $challenge['expires'], 'challenge_expires' => $challenge['expires'],
] ]
); );
@@ -160,8 +156,9 @@ class Provider extends AuthenticationProviderAbstract
public function verifyChallenge(ProviderContext $context, string $code): ProviderResult public function verifyChallenge(ProviderContext $context, string $code): ProviderResult
{ {
$tenantId = $context->tenantId; $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)) { if (empty($tenantId)) {
return ProviderResult::failed( return ProviderResult::failed(
ProviderResult::ERROR_INTERNAL, ProviderResult::ERROR_INTERNAL,
@@ -169,10 +166,10 @@ class Provider extends AuthenticationProviderAbstract
); );
} }
if (empty($userIdentity)) { if (empty($recipientAddress)) {
return ProviderResult::failed( return ProviderResult::failed(
ProviderResult::ERROR_INVALID_CREDENTIALS, 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); $code = preg_replace('/[\s\-]/', '', $code);
// Verify the challenge // Verify the challenge
$result = $this->challengeStore->verify($tenantId, $userIdentity, $code); $result = $this->challengeStore->verify($tenantId, $recipientAddress, $code);
if (!$result['success']) { if (!$result['success']) {
$this->logger->debug('Email challenge verification failed', [ $this->logger->debug('Email challenge verification failed', [
'tenantId' => $tenantId, 'tenantId' => $tenantId,
'error' => $result['error'] ?? 'Unknown', 'error' => $result['error'] ?? 'Unknown',
]); ]);
return ProviderResult::failed( return ProviderResult::failed(
ProviderResult::ERROR_FACTOR_FAILED, ProviderResult::ERROR_FACTOR_FAILED,
$result['error'] ?? 'Invalid verification code' $result['error'] ?? 'Invalid verification code'
@@ -199,9 +196,9 @@ class Provider extends AuthenticationProviderAbstract
]); ]);
return ProviderResult::success([ return ProviderResult::success([
'identity' => $userIdentity, 'identity' => $context->userIdentity ?? $recipientAddress,
'provider' => $this->identifier(), 'provider' => $this->identifier(),
'verified_email' => $userIdentity, 'verified_email' => $recipientAddress,
]); ]);
} }
@@ -213,6 +210,32 @@ class Provider extends AuthenticationProviderAbstract
return $this->verifyChallenge($context, $secret); 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 // Email Sending
// ========================================================================= // =========================================================================