|null $request */ public function getAuthToken(?array $request = null): string { $this->detectToken(); if ($request !== null) { return (new Request($request))->getStringParameter('token_auth', ''); } return $this->authToken; } /** * Returns true if a token_auth parameter was supplied via a secure mechanism and is not present as a URL parameter * * @return bool True if token was supplied in a secure way */ public function wasTokenAuthProvidedSecurely(): bool { $this->detectToken(); return $this->wasTokenProvidedSecurely; } public function isSessionToken(): bool { $this->detectToken(); return $this->isSessionToken; } private function detectToken(): void { $this->validateNoConflictingAuthParameters(); $this->initTokenFromHeader() || $this->initTokenFromJsonRequestBody() || $this->initTokenFromPostRequest() || $this->initTokenFromGetRequest(); } private function validateNoConflictingAuthParameters(): void { if ($this->isConflictingAuthValidationDone || $this->shouldSkipConflictingAuthValidation()) { return; } $this->isConflictingAuthValidationDone = true; $tokenAuthBySource = []; $forceApiSessionBySource = []; $headerTokenAuth = $this->getTokenAuthFromHeader(); if (!empty($headerTokenAuth)) { $tokenAuthBySource['header'] = $headerTokenAuth; } $jsonTokenAuth = $this->getTokenAuthFromJsonRequestBody(); if (!empty($jsonTokenAuth)) { $tokenAuthBySource['json'] = $jsonTokenAuth; } $post = Request::fromPost(); $postTokenAuth = $post->getStringParameter('token_auth', ''); if (!empty($postTokenAuth)) { $tokenAuthBySource['post'] = $postTokenAuth; } if (array_key_exists('force_api_session', $_POST)) { $forceApiSessionBySource['post'] = $post->getBoolParameter('force_api_session', false); } $get = Request::fromGet(); if (!$this->isNavigationOnlyEndpoint()) { $getTokenAuth = $get->getStringParameter('token_auth', ''); if (!empty($getTokenAuth)) { $tokenAuthBySource['get'] = $getTokenAuth; } if (array_key_exists('force_api_session', $_GET)) { $forceApiSessionBySource['get'] = $get->getBoolParameter('force_api_session', false); } } $this->throwIfValuesConflict($tokenAuthBySource); $this->throwIfValuesConflict($forceApiSessionBySource); } private function shouldSkipConflictingAuthValidation(): bool { return ApiRequest::isRootRequestApiRequest() && !ApiRequest::isCurrentApiRequestTheRootApiRequest(); } /** * @param array $valuesBySource */ private function throwIfValuesConflict(array $valuesBySource): void { if (count($valuesBySource) < 2) { return; } $firstValue = array_shift($valuesBySource); foreach ($valuesBySource as $value) { if ($value !== $firstValue) { throw new BadRequestException(Piwik::translate('General_ConflictingAuthenticationParametersProvided')); } } } private function initTokenFromHeader(): bool { $tokenAuth = $this->getTokenAuthFromHeader(); if ($tokenAuth !== null) { $this->authToken = $tokenAuth; $this->wasTokenProvidedSecurely = true; return true; } return false; } private function initTokenFromJsonRequestBody(): bool { $tokenAuth = $this->getTokenAuthFromJsonRequestBody(); if (!empty($tokenAuth)) { $this->authToken = $tokenAuth; $this->wasTokenProvidedSecurely = true; return true; } return false; } private function initTokenFromPostRequest(): bool { $request = Request::fromPost(); $tokenAuth = $request->getStringParameter('token_auth', ''); if ($tokenAuth !== '') { $this->authToken = $tokenAuth; $this->wasTokenProvidedSecurely = true; $this->isSessionToken = $request->getBoolParameter('force_api_session', false); return true; } return false; } private function initTokenFromGetRequest(): bool { if ($this->isNavigationOnlyEndpoint()) { return false; } $request = Request::fromGet(); $tokenAuth = $request->getStringParameter('token_auth', ''); if ($tokenAuth !== '') { $this->authToken = $tokenAuth; $this->wasTokenProvidedSecurely = false; $this->isSessionToken = $request->getBoolParameter('force_api_session', false); return true; } return false; } /** * Some endpoints exist only as browser navigations, not as API entry points. They are reached * via a top-level GET and hand off to another page, so GET credentials are not part of their * request contract and must not be consumed as authentication. * * Keep this list extremely small. Only add an endpoint here once it has been independently * confirmed that the endpoint never needs URL-borne auth and never performs writes. */ private function isNavigationOnlyEndpoint(): bool { if (SettingsServer::isTrackerApiRequest()) { return false; } $get = Request::fromGet(); $module = $get->getStringParameter('module', ''); $action = $get->getStringParameter('action', ''); return $module === 'Overlay' && $action === 'startOverlaySession'; } private function getTokenAuthFromHeader(): ?string { if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer ') === 0) { return substr($_SERVER['HTTP_AUTHORIZATION'], 7); } return null; } private function getTokenAuthFromJsonRequestBody(): ?string { if ($this->isJsonRequestBodyTokenLoaded) { return $this->jsonRequestBodyTokenAuth; } $this->isJsonRequestBodyTokenLoaded = true; $this->jsonRequestBodyTokenAuth = null; // Token in JSON request body is only supported for tracking requests if (!SettingsServer::isTrackerApiRequest()) { return null; } $requestBody = file_get_contents('php://input'); if (!empty($requestBody) && strpos($requestBody, '{') === 0) { $jsonContent = json_decode($requestBody, true); if (is_array($jsonContent) && !empty($jsonContent['token_auth']) && is_string($jsonContent['token_auth'])) { $this->jsonRequestBodyTokenAuth = $jsonContent['token_auth']; } } return $this->jsonRequestBodyTokenAuth; } }