Fix: use users profile mail address
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
+56
-33
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user