Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use MiniShop3\MiniShop3;
use MiniShop3\Model\msCustomer;
use MiniShop3\Router\Response;
use MiniShop3\Services\Customer\AuthManager;
use MiniShop3\Services\Customer\EmailVerificationService;
use MODX\Revolution\modX;

Expand Down Expand Up @@ -113,8 +114,22 @@ public function verify(array $params): array|Response
return $this->error($this->modx->lexicon('ms3_customer_err_email_verification_invalid'));
}

$_SESSION['ms3']['customer_id'] = $customer->id;
$_SESSION['ms3']['customer_token'] = $customer->get('token');
/** @var AuthManager $authManager */
$authManager = $this->modx->services->get('ms3_auth_manager');
$session = $authManager->establishApiSession($customer);

if (!$session) {
$this->modx->log(
modX::LOG_LEVEL_ERROR,
"[CustomerEmailController] Email verified for customer #{$customer->id} but API session establish failed"
);
if ($htmlFlow && !$formatJson) {
// Email already verified; do not send user to the verification-failed page
return Response::redirect($this->buildEmailVerificationSuccessRedirectUrl(), 302);
}

return $this->error($this->modx->lexicon('ms3_customer_err_token_create'));
}

$this->modx->log(
modX::LOG_LEVEL_INFO,
Expand All @@ -127,7 +142,11 @@ public function verify(array $params): array|Response

return $this->success(
$this->modx->lexicon('ms3_customer_email_verify_success'),
['customer_id' => $customer->id]
[
'customer_id' => $customer->id,
'token' => $session['token'],
'expires_at' => $session['expires_at'],
]
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,10 +549,10 @@ protected function createFromOrderData(array $orderData): ?msCustomer
if ($msCustomer && $autoLogin) {
/** @var AuthManager $authManager */
$authManager = $this->modx->services->get('ms3_auth_manager');
if (!$authManager->establishCustomerSession($msCustomer)) {
if (!$authManager->establishApiSession($msCustomer)) {
$this->modx->log(
modX::LOG_LEVEL_ERROR,
"[Customer] establishCustomerSession failed for customer #{$msCustomer->id}"
"[Customer] establishApiSession failed for customer #{$msCustomer->id}"
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public function process()

$rateLimiter->reset('login', $ip);

$session = $authManager->establishCustomerSession($customer);
$session = $authManager->establishApiSession($customer);
if (!$session) {
return $this->failure($this->modx->lexicon('ms3_customer_err_token_create'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* Register - processor for registering a new customer
*
* Creates a new customer with validation and optional email verification.
* On auto-login: binds existing session token to new customer (preserves guest cart).
* On auto-login: rotates API session token (anti token-fixation); guest cart may transfer.
* Protected from spam via RateLimiter.
*
* @package MiniShop3\Processors\Api\Customer
Expand Down Expand Up @@ -82,34 +82,31 @@ public function process()

$tokenString = null;
$expiresAt = null;
$redirectUrl = '';

if ($autoLogin && !$requireEmailVerification) {
/** @var AuthManager $authManager */
$authManager = $this->modx->services->get('ms3_auth_manager');
$session = $authManager->establishCustomerSession($customer);
$session = $authManager->establishApiSession($customer);

if (!$session) {
return $this->failure($this->modx->lexicon('ms3_customer_err_token_create'));
}

$tokenString = $session['token'];
$expiresAt = $session['expires_at'];
}

$rateLimiter->reset('login', $ip);

$redirectUrl = '';
if ($autoLogin && !$requireEmailVerification) {
$redirectPageId = (int)$this->getProperty('redirect_page_id', 0);
if (!$redirectPageId) {
$redirectPageId = (int)$this->modx->getOption('ms3_customer_redirect_after_login', null, 0);
}

if ($redirectPageId > 0) {
$redirectUrl = $this->modx->makeUrl($redirectPageId, '', '', 'full');
}
}

$rateLimiter->reset('login', $ip);

return $this->success($this->modx->lexicon('ms3_customer_register_success'), [
'customer' => [
'id' => $customer->id,
Expand All @@ -125,5 +122,4 @@ public function process()
'redirect_url' => $redirectUrl,
]);
}

}
30 changes: 30 additions & 0 deletions core/components/minishop3/src/Services/Customer/AuthManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,36 @@ public function validateToken(string $tokenString, string $type = 'api'): ?msCus
return $customer;
}

/**
* Whether a previous API token row may be migrated/revoked for this customer.
*
* null = no row (orphan draft by token string); 0 = guest; same id = own session.
* Foreign customer_id: no (do not steal or revoke another session).
*
* @param int|null $previousOwnerId customer_id on the previous token row; null if no row
*/
public static function canMigratePreviousApiToken(?int $previousOwnerId, int $customerId): bool
{
return $previousOwnerId === null
|| $previousOwnerId === 0
|| $previousOwnerId === $customerId;
}

/**
* Issue a fresh API token after login/register/verify (token rotation).
*
* Compatibility alias for establishCustomerSession() (TokenService-backed).
* $previousToken is ignored: the presented cookie/session token is resolved
* inside establishCustomerSession via TokenService::getBindableTokenString().
*
* @param string|null $previousToken unused; kept for call-site compatibility
* @return array{token: string, expires_at: string}|null
*/
public function establishApiSession(msCustomer $customer, ?string $previousToken = null): ?array
{
return $this->establishCustomerSession($customer);
}

/**
* Revoke all customer tokens of specific type
*
Expand Down
60 changes: 60 additions & 0 deletions core/components/minishop3/tests/ApiTokenRotationPolicyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* AuthManager API token rotation policy (no MODX).
*
* Запуск: php tests/ApiTokenRotationPolicyTest.php
*/

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use MiniShop3\Services\Customer\AuthManager;

$fail = static function (string $message): never {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
};

// Guest token (customer_id 0) — migrate + revoke (fixes token fixation)
if (!AuthManager::canMigratePreviousApiToken(0, 42)) {
$fail('guest token must be migratable');
}

// Own previous session token — rotate
if (!AuthManager::canMigratePreviousApiToken(42, 42)) {
$fail('own token must be migratable');
}

// Orphan draft (no API token row) — allow draft migrate by string
if (!AuthManager::canMigratePreviousApiToken(null, 42)) {
$fail('null owner (orphan) must allow migrate');
}

// Foreign customer token — must NOT revoke/steal
if (AuthManager::canMigratePreviousApiToken(7, 42)) {
$fail('foreign token must not be migratable');
}

// Regression guard: must rotate via establishApiSession, never rebind guest token
$loginSrc = file_get_contents(__DIR__ . '/../src/Processors/Api/Customer/Login.php');
$registerSrc = file_get_contents(__DIR__ . '/../src/Processors/Api/Customer/Register.php');
$customerSrc = file_get_contents(__DIR__ . '/../src/Controllers/Customer/Customer.php');
foreach (['Login' => $loginSrc, 'Register' => $registerSrc] as $label => $src) {
if (!str_contains($src, 'establishApiSession')) {
$fail("{$label} must call establishApiSession");
}
if (preg_match("/set\\(\\s*'customer_id'\\s*,/", $src)) {
$fail("{$label} must not rebind token via set('customer_id')");
}
}
if (!str_contains($customerSrc, 'establishApiSession')) {
$fail('Customer auto-login must use establishApiSession');
}
if (preg_match('/function autoLoginCustomer.*?\{.*?set\(\s*\'customer_id\'/s', $customerSrc)) {
$fail('autoLoginCustomer must not rebind token via set(customer_id)');
}

fwrite(STDOUT, "OK ApiTokenRotationPolicyTest\n");
exit(0);