diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php index 89f62f19..12ebe815 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerEmailController.php @@ -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; @@ -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, @@ -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'], + ] ); } diff --git a/core/components/minishop3/src/Controllers/Customer/Customer.php b/core/components/minishop3/src/Controllers/Customer/Customer.php index dd76b3fc..5b451b1e 100644 --- a/core/components/minishop3/src/Controllers/Customer/Customer.php +++ b/core/components/minishop3/src/Controllers/Customer/Customer.php @@ -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}" ); } } diff --git a/core/components/minishop3/src/Processors/Api/Customer/Login.php b/core/components/minishop3/src/Processors/Api/Customer/Login.php index 6e8545a5..1fc15b7f 100644 --- a/core/components/minishop3/src/Processors/Api/Customer/Login.php +++ b/core/components/minishop3/src/Processors/Api/Customer/Login.php @@ -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')); } diff --git a/core/components/minishop3/src/Processors/Api/Customer/Register.php b/core/components/minishop3/src/Processors/Api/Customer/Register.php index 295b5323..d9c05138 100644 --- a/core/components/minishop3/src/Processors/Api/Customer/Register.php +++ b/core/components/minishop3/src/Processors/Api/Customer/Register.php @@ -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 @@ -82,11 +82,12 @@ 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')); @@ -94,22 +95,18 @@ public function process() $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, @@ -125,5 +122,4 @@ public function process() 'redirect_url' => $redirectUrl, ]); } - } diff --git a/core/components/minishop3/src/Services/Customer/AuthManager.php b/core/components/minishop3/src/Services/Customer/AuthManager.php index 2f415433..f4be1af3 100644 --- a/core/components/minishop3/src/Services/Customer/AuthManager.php +++ b/core/components/minishop3/src/Services/Customer/AuthManager.php @@ -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 * diff --git a/core/components/minishop3/tests/ApiTokenRotationPolicyTest.php b/core/components/minishop3/tests/ApiTokenRotationPolicyTest.php new file mode 100644 index 00000000..fafcb897 --- /dev/null +++ b/core/components/minishop3/tests/ApiTokenRotationPolicyTest.php @@ -0,0 +1,60 @@ + $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);