diff --git a/core/components/minishop3/config/routes/web.php b/core/components/minishop3/config/routes/web.php index df64d7eb..c4e41dad 100644 --- a/core/components/minishop3/config/routes/web.php +++ b/core/components/minishop3/config/routes/web.php @@ -47,94 +47,94 @@ $modx->getOption('ms3_rate_limit_decay_seconds', null, 60) ); $serviceCheckMiddleware = new ServiceCheckMiddleware($modx); -$router->group('/api/v1', function($router) use ($modx, $tokenMiddleware) { +$router->group('/api/v1', function ($router) use ($modx, $tokenMiddleware) { - $router->group('/cart', function($router) use ($modx) { - $router->post('/add', function($params) use ($modx) { + $router->group('/cart', function ($router) use ($modx) { + $router->post('/add', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CartController($modx); return $controller->add($params); }); - $router->post('/remove', function($params) use ($modx) { + $router->post('/remove', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CartController($modx); return $controller->remove($params); }); - $router->post('/change', function($params) use ($modx) { + $router->post('/change', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CartController($modx); return $controller->change($params); }); - $router->get('/get', function($params) use ($modx) { + $router->get('/get', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CartController($modx); return $controller->get($params); }); - $router->post('/clean', function($params) use ($modx) { + $router->post('/clean', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CartController($modx); return $controller->clean($params); }); }, [$tokenMiddleware]); - $router->group('/order', function($router) use ($modx) { - $router->get('/get', function($params) use ($modx) { + $router->group('/order', function ($router) use ($modx) { + $router->get('/get', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->get($params); }); - $router->post('/add', function($params) use ($modx) { + $router->post('/add', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->add($params); }); - $router->post('/set', function($params) use ($modx) { + $router->post('/set', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->set($params); }); - $router->post('/remove', function($params) use ($modx) { + $router->post('/remove', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->remove($params); }); - $router->post('/submit', function($params) use ($modx) { + $router->post('/submit', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->submit($params); }); - $router->post('/clean', function($params) use ($modx) { + $router->post('/clean', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->clean($params); }); - $router->get('/cost', function($params) use ($modx) { + $router->get('/cost', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->getCost($params); }); - $router->get('/cost/cart', function($params) use ($modx) { + $router->get('/cost/cart', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->getCartCost($params); }); - $router->get('/cost/delivery', function($params) use ($modx) { + $router->get('/cost/delivery', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->getDeliveryCost($params); }); - $router->get('/cost/payment', function($params) use ($modx) { + $router->get('/cost/payment', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->getPaymentCost($params); }); - $router->post('/address/set', function($params) use ($modx) { + $router->post('/address/set', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->setCustomerAddress($params); }); - $router->post('/address/clean', function($params) use ($modx) { + $router->post('/address/clean', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->cleanCustomerAddress($params); }); - $router->get('/delivery/validation-rules', function($params) use ($modx) { + $router->get('/delivery/validation-rules', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->getDeliveryValidationRules($params); }); - $router->get('/delivery/required-fields', function($params) use ($modx) { + $router->get('/delivery/required-fields', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->getDeliveryRequiresFields($params); }); }, [$tokenMiddleware]); - $router->group('/customer', function($router) use ($modx, $tokenMiddleware) { - $router->post('/login', function($params) use ($modx) { + $router->group('/customer', function ($router) use ($modx, $tokenMiddleware) { + $router->post('/login', function ($params) use ($modx) { $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -155,7 +155,7 @@ return Response::success($response->getObject(), $response->getMessage()); }); - $router->post('/logout', function($params) use ($modx) { + $router->post('/logout', function ($params) use ($modx) { $response = $modx->runProcessor( 'MiniShop3\Processors\Api\Customer\Logout', [] @@ -167,7 +167,7 @@ return Response::success($response->getObject() ?: [], $response->getMessage()); }, [$tokenMiddleware]); - $router->post('/register', function($params) use ($modx) { + $router->post('/register', function ($params) use ($modx) { $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -197,7 +197,7 @@ return Response::success($response->getObject(), $response->getMessage()); }); - $router->post('/add', function($params) use ($modx) { + $router->post('/add', function ($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -206,7 +206,7 @@ return $controller->updateField($data); }, [$tokenMiddleware]); - $router->get('/token/get', function($params) use ($modx) { + $router->get('/token/get', function ($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $ms3->initialize(); $response = $ms3->customer->generateToken(); @@ -218,21 +218,21 @@ } }); - $router->post('/token/refresh', function($params) use ($modx) { + $router->post('/token/refresh', function ($params) use ($modx) { return Response::success(['message' => 'Customer token/refresh endpoint - not implemented yet']); }); - $router->group('/addresses', function($router) use ($modx) { - $router->get('', function($params) use ($modx) { + $router->group('/addresses', function ($router) use ($modx) { + $router->get('', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CustomerAddressController($modx); return $controller->getList($params); }); - $router->get('/{id}', function($params) use ($modx) { + $router->get('/{id}', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CustomerAddressController($modx); return $controller->get($params); }); - $router->post('', function($params) use ($modx) { + $router->post('', function ($params) use ($modx) { $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -240,7 +240,7 @@ return $controller->create($data); }); - $router->put('/{id}', function($params) use ($modx) { + $router->put('/{id}', function ($params) use ($modx) { $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -249,18 +249,18 @@ $controller = new \MiniShop3\Controllers\Api\Web\CustomerAddressController($modx); return $controller->update($data); }); - $router->delete('/{id}', function($params) use ($modx) { + $router->delete('/{id}', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CustomerAddressController($modx); return $controller->delete($params); }); - $router->put('/{id}/set-default', function($params) use ($modx) { + $router->put('/{id}/set-default', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CustomerAddressController($modx); return $controller->setDefault($params); }); }, [$tokenMiddleware]); - $router->put('/profile', function($params) use ($modx) { + $router->put('/profile', function ($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -269,41 +269,41 @@ return $controller->update($data); }, [$tokenMiddleware]); - $router->post('/changeAddress', function($params) use ($modx) { + $router->post('/changeAddress', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\OrderController($modx); return $controller->changeCustomerAddress($params); }, [$tokenMiddleware]); - $router->post('/email/resend-verification', function($params) use ($modx) { + $router->post('/email/resend-verification', function ($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $controller = new \MiniShop3\Controllers\Api\Web\CustomerEmailController($modx, $ms3); return $controller->resendVerification(); }, [$tokenMiddleware]); - $router->get('/email/verify', function($params) use ($modx) { + $router->get('/email/verify', function ($params) use ($modx) { $ms3 = $modx->services->get('ms3'); $controller = new \MiniShop3\Controllers\Api\Web\CustomerEmailController($modx, $ms3); return $controller->verify($params); }); - $router->post('/orders/{id}/cancel', function($params) use ($modx) { + $router->post('/orders/{id}/cancel', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CustomerOrderController($modx); return $controller->cancel($params); }, [$tokenMiddleware]); }); - $router->group('/product', function($router) use ($modx) { + $router->group('/product', function ($router) use ($modx) { - $router->get('/get/{id}', function($params) use ($modx) { + $router->get('/get/{id}', function ($params) use ($modx) { return Response::success(['message' => 'Product get endpoint - not implemented yet', 'id' => $params['id'] ?? null]); }); - $router->get('/list', function($params) use ($modx) { + $router->get('/list', function ($params) use ($modx) { return Response::success(['message' => 'Product list endpoint - not implemented yet']); }); }); - $router->get('/health', function() use ($modx) { + $router->get('/health', function () use ($modx) { return Response::success([ 'status' => 'ok', 'version' => $modx->getOption('ms3_version', null, '1.0.0'), diff --git a/core/components/minishop3/src/Controllers/Api/Web/AuthorizedCustomerTrait.php b/core/components/minishop3/src/Controllers/Api/Web/AuthorizedCustomerTrait.php index 6dae593e..86497a8a 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/AuthorizedCustomerTrait.php +++ b/core/components/minishop3/src/Controllers/Api/Web/AuthorizedCustomerTrait.php @@ -13,10 +13,9 @@ trait AuthorizedCustomerTrait { /** - * Get authorized customer (session or API token) + * Get authorized customer via validated API token only. * - * Method 1: API token (ms3_token in request or session). - * Method 2: Session customer_id (set by TokenMiddleware). + * Method 1: API token (ms3_token in request or session customer_token cache). * * @return msCustomer|null */ @@ -25,34 +24,24 @@ protected function getAuthorizedCustomer(): ?msCustomer $ms3 = $this->modx->services->get('ms3'); $ms3->initialize(); - // Method 1: Try API token $tokenString = $_REQUEST['ms3_token'] ?? $_SESSION['ms3']['customer_token'] ?? ''; - $tokenPresented = $tokenString !== ''; - - if ($tokenPresented) { - /** @var TokenService $tokenService */ - $tokenService = $this->modx->services->get('ms3_token_service'); - $resolved = $tokenService->resolveApiToken($tokenString); + if ($tokenString === '') { + return null; + } - if ($resolved['reason'] === 'ok') { - $customer = $this->modx->getObject(msCustomer::class, $resolved['token']->get('customer_id')); - if ($customer) { - return $customer; - } - } + /** @var TokenService $tokenService */ + $tokenService = $this->modx->services->get('ms3_token_service'); + $resolved = $tokenService->resolveApiToken($tokenString); - // Explicit token was rejected — do not fall back to session customer_id. + if ($resolved['reason'] !== 'ok') { return null; } - // Method 2: Fall back to session customer_id (consistent with TokenMiddleware) - if (!empty($_SESSION['ms3']['customer_id'])) { - $customer = $this->modx->getObject(msCustomer::class, (int)$_SESSION['ms3']['customer_id']); - if ($customer) { - return $customer; - } + $customerId = (int) $resolved['token']->get('customer_id'); + if ($customerId <= 0) { + return null; } - return null; + return $this->modx->getObject(msCustomer::class, $customerId) ?: null; } } diff --git a/core/components/minishop3/src/Middleware/TokenMiddleware.php b/core/components/minishop3/src/Middleware/TokenMiddleware.php index 1393931a..ac090978 100644 --- a/core/components/minishop3/src/Middleware/TokenMiddleware.php +++ b/core/components/minishop3/src/Middleware/TokenMiddleware.php @@ -36,6 +36,7 @@ class TokenMiddleware implements MiddlewareInterface '/api/v1/product/list', '/api/v1/customer/token/get', '/api/v1/customer/token/refresh', + '/api/v1/customer/logout', '/api/v1/health', ]; @@ -118,22 +119,8 @@ public function handle(array $params) return Response::error('ms3_err_token_invalid', HttpStatus::UNAUTHORIZED); } } elseif (!$isPublic && !empty($_SESSION['ms3']['customer_id'])) { - // No token in request: allow existing session customer (browser session). - $customerId = (int)$_SESSION['ms3']['customer_id']; - $customer = $this->modx->getObject(\MiniShop3\Model\msCustomer::class, $customerId); - if ( - $customer - && $this->isCustomerSessionAllowed($customer) - && $tokenService->sessionTokenBelongsToCustomer($customerId) - ) { - return null; - } - - unset( - $_SESSION['ms3']['customer_id'], - $_SESSION['ms3']['customer_token'], - $_SESSION['ms3']['customer_token_expires'] - ); + // Stale session identity without a resolvable token must not bypass revoke. + $this->clearClientTokenState(); } // No valid token found @@ -188,30 +175,16 @@ private function resolveToken(): string } // 3. $_REQUEST (includes cookie via injection + legacy URL param) - return $_REQUEST['ms3_token'] ?? $_REQUEST['token'] ?? ''; - } - - /** - * Session shortcut is valid only for active, non-blocked customers. - */ - private function isCustomerSessionAllowed(\MiniShop3\Model\msCustomer $customer): bool - { - if (!$customer->get('is_active')) { - return false; - } - - if (!$customer->get('is_blocked')) { - return true; - } - - $blockedUntil = $customer->get('blocked_until'); - if ($blockedUntil && strtotime((string)$blockedUntil) > time()) { - return false; + $token = $_REQUEST['ms3_token'] ?? $_REQUEST['token'] ?? ''; + if (!empty($token)) { + return $token; } - return true; + // 4. Session cache (must still pass DB validation in handle()) + return $_SESSION['ms3']['customer_token'] ?? ''; } + /** * Check if route is public * diff --git a/core/components/minishop3/src/Processors/Api/Customer/Logout.php b/core/components/minishop3/src/Processors/Api/Customer/Logout.php index 55af67c3..51c467fe 100644 --- a/core/components/minishop3/src/Processors/Api/Customer/Logout.php +++ b/core/components/minishop3/src/Processors/Api/Customer/Logout.php @@ -2,7 +2,11 @@ namespace MiniShop3\Processors\Api\Customer; +use MiniShop3\Model\msCustomer; use MiniShop3\Services\Customer\AuthManager; +use MiniShop3\Services\TokenService; +use MiniShop3\Utils\CookieHelper; +use MiniShop3\Utils\SessionHelper; use MODX\Revolution\Processors\Processor; /** @@ -22,6 +26,16 @@ public function process() { $this->modx->lexicon->load('minishop3:customer'); + $customer = $this->resolveCustomerForLogout(); + if ($customer) { + // Ensure AuthManager sees Bearer/cookie identity when PHP session is empty. + SessionHelper::ensureActive(); + if (!isset($_SESSION['ms3']) || !is_array($_SESSION['ms3'])) { + $_SESSION['ms3'] = []; + } + $_SESSION['ms3']['customer_id'] = (int) $customer->id; + } + /** @var AuthManager $authManager */ $authManager = $this->modx->services->get('ms3_auth_manager'); @@ -29,6 +43,66 @@ public function process() return $this->failure($this->modx->lexicon('ms3_customer_err_token_create')); } + if (session_status() === PHP_SESSION_ACTIVE) { + session_regenerate_id(true); + } + return $this->success($this->modx->lexicon('ms3_customer_logout_success')); } + + /** + * Resolve customer from session (after TokenMiddleware) or validated API token. + */ + protected function resolveCustomerForLogout(): ?msCustomer + { + $customerId = (int) ($_SESSION['ms3']['customer_id'] ?? 0); + if ($customerId > 0) { + $customer = $this->modx->getObject(msCustomer::class, $customerId); + if ($customer) { + return $customer; + } + } + + $tokenString = $this->resolveLogoutTokenString(); + if ($tokenString === '') { + return null; + } + + /** @var TokenService $tokenService */ + $tokenService = $this->modx->services->get('ms3_token_service'); + $resolved = $tokenService->resolveApiToken($tokenString); + if ($resolved['reason'] !== 'ok') { + return null; + } + + $tokenCustomerId = (int) $resolved['token']->get('customer_id'); + if ($tokenCustomerId <= 0) { + return null; + } + + return $this->modx->getObject(msCustomer::class, $tokenCustomerId) ?: null; + } + + protected function resolveLogoutTokenString(): string + { + $cookieToken = CookieHelper::getTokenFromCookie(); + if ($cookieToken !== '') { + return $cookieToken; + } + + $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; + if (str_starts_with($authHeader, 'Bearer ')) { + $bearer = trim(substr($authHeader, 7)); + if ($bearer !== '') { + return $bearer; + } + } + + $legacyHeader = $_SERVER['HTTP_MS3TOKEN'] ?? ''; + if ($legacyHeader !== '') { + return $legacyHeader; + } + + return $_REQUEST['ms3_token'] ?? $_SESSION['ms3']['customer_token'] ?? ''; + } } diff --git a/core/components/minishop3/tests/TokenSessionRevokePolicyTest.php b/core/components/minishop3/tests/TokenSessionRevokePolicyTest.php new file mode 100644 index 00000000..21071991 --- /dev/null +++ b/core/components/minishop3/tests/TokenSessionRevokePolicyTest.php @@ -0,0 +1,82 @@ +