PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Request / AuthenticationToken.php
matomo / app / core / Request Last commit date
AuthenticationToken.php 2 weeks ago
AuthenticationToken.php
213 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Request;
10
11 use Piwik\API\Request as ApiRequest;
12 use Piwik\Http\BadRequestException;
13 use Piwik\Piwik;
14 use Piwik\Request;
15 use Piwik\SettingsServer;
16 /**
17 * Main class to handle actions related to auth tokens.
18 */
19 class AuthenticationToken
20 {
21 /** @var string */
22 protected $authToken = '';
23 /** @var bool */
24 protected $wasTokenProvidedSecurely = \false;
25 /** @var bool */
26 protected $isSessionToken = \false;
27 /** @var bool */
28 protected $isConflictingAuthValidationDone = \false;
29 /** @var bool */
30 protected $isJsonRequestBodyTokenLoaded = \false;
31 /** @var string|null */
32 protected $jsonRequestBodyTokenAuth = null;
33 /**
34 * @param array<string, mixed>|null $request
35 */
36 public function getAuthToken(?array $request = null) : string
37 {
38 $this->detectToken();
39 if ($request !== null) {
40 return (new Request($request))->getStringParameter('token_auth', '');
41 }
42 return $this->authToken;
43 }
44 /**
45 * Returns true if a token_auth parameter was supplied via a secure mechanism and is not present as a URL parameter
46 *
47 * @return bool True if token was supplied in a secure way
48 */
49 public function wasTokenAuthProvidedSecurely() : bool
50 {
51 $this->detectToken();
52 return $this->wasTokenProvidedSecurely;
53 }
54 public function isSessionToken() : bool
55 {
56 $this->detectToken();
57 return $this->isSessionToken;
58 }
59 private function detectToken() : void
60 {
61 $this->validateNoConflictingAuthParameters();
62 $this->initTokenFromHeader() || $this->initTokenFromJsonRequestBody() || $this->initTokenFromPostRequest() || $this->initTokenFromGetRequest();
63 }
64 private function validateNoConflictingAuthParameters() : void
65 {
66 if ($this->isConflictingAuthValidationDone || $this->shouldSkipConflictingAuthValidation()) {
67 return;
68 }
69 $this->isConflictingAuthValidationDone = \true;
70 $tokenAuthBySource = [];
71 $forceApiSessionBySource = [];
72 $headerTokenAuth = $this->getTokenAuthFromHeader();
73 if (!empty($headerTokenAuth)) {
74 $tokenAuthBySource['header'] = $headerTokenAuth;
75 }
76 $jsonTokenAuth = $this->getTokenAuthFromJsonRequestBody();
77 if (!empty($jsonTokenAuth)) {
78 $tokenAuthBySource['json'] = $jsonTokenAuth;
79 }
80 $post = Request::fromPost();
81 $postTokenAuth = $post->getStringParameter('token_auth', '');
82 if (!empty($postTokenAuth)) {
83 $tokenAuthBySource['post'] = $postTokenAuth;
84 }
85 if (array_key_exists('force_api_session', $_POST)) {
86 $forceApiSessionBySource['post'] = $post->getBoolParameter('force_api_session', \false);
87 }
88 $get = Request::fromGet();
89 if (!$this->isNavigationOnlyEndpoint()) {
90 $getTokenAuth = $get->getStringParameter('token_auth', '');
91 if (!empty($getTokenAuth)) {
92 $tokenAuthBySource['get'] = $getTokenAuth;
93 }
94 if (array_key_exists('force_api_session', $_GET)) {
95 $forceApiSessionBySource['get'] = $get->getBoolParameter('force_api_session', \false);
96 }
97 }
98 $this->throwIfValuesConflict($tokenAuthBySource);
99 $this->throwIfValuesConflict($forceApiSessionBySource);
100 }
101 private function shouldSkipConflictingAuthValidation() : bool
102 {
103 return ApiRequest::isRootRequestApiRequest() && !ApiRequest::isCurrentApiRequestTheRootApiRequest();
104 }
105 /**
106 * @param array<string, bool|string> $valuesBySource
107 */
108 private function throwIfValuesConflict(array $valuesBySource) : void
109 {
110 if (count($valuesBySource) < 2) {
111 return;
112 }
113 $firstValue = array_shift($valuesBySource);
114 foreach ($valuesBySource as $value) {
115 if ($value !== $firstValue) {
116 throw new BadRequestException(Piwik::translate('General_ConflictingAuthenticationParametersProvided'));
117 }
118 }
119 }
120 private function initTokenFromHeader() : bool
121 {
122 $tokenAuth = $this->getTokenAuthFromHeader();
123 if ($tokenAuth !== null) {
124 $this->authToken = $tokenAuth;
125 $this->wasTokenProvidedSecurely = \true;
126 return \true;
127 }
128 return \false;
129 }
130 private function initTokenFromJsonRequestBody() : bool
131 {
132 $tokenAuth = $this->getTokenAuthFromJsonRequestBody();
133 if (!empty($tokenAuth)) {
134 $this->authToken = $tokenAuth;
135 $this->wasTokenProvidedSecurely = \true;
136 return \true;
137 }
138 return \false;
139 }
140 private function initTokenFromPostRequest() : bool
141 {
142 $request = Request::fromPost();
143 $tokenAuth = $request->getStringParameter('token_auth', '');
144 if ($tokenAuth !== '') {
145 $this->authToken = $tokenAuth;
146 $this->wasTokenProvidedSecurely = \true;
147 $this->isSessionToken = $request->getBoolParameter('force_api_session', \false);
148 return \true;
149 }
150 return \false;
151 }
152 private function initTokenFromGetRequest() : bool
153 {
154 if ($this->isNavigationOnlyEndpoint()) {
155 return \false;
156 }
157 $request = Request::fromGet();
158 $tokenAuth = $request->getStringParameter('token_auth', '');
159 if ($tokenAuth !== '') {
160 $this->authToken = $tokenAuth;
161 $this->wasTokenProvidedSecurely = \false;
162 $this->isSessionToken = $request->getBoolParameter('force_api_session', \false);
163 return \true;
164 }
165 return \false;
166 }
167 /**
168 * Some endpoints exist only as browser navigations, not as API entry points. They are reached
169 * via a top-level GET and hand off to another page, so GET credentials are not part of their
170 * request contract and must not be consumed as authentication.
171 *
172 * Keep this list extremely small. Only add an endpoint here once it has been independently
173 * confirmed that the endpoint never needs URL-borne auth and never performs writes.
174 */
175 private function isNavigationOnlyEndpoint() : bool
176 {
177 if (SettingsServer::isTrackerApiRequest()) {
178 return \false;
179 }
180 $get = Request::fromGet();
181 $module = $get->getStringParameter('module', '');
182 $action = $get->getStringParameter('action', '');
183 return $module === 'Overlay' && $action === 'startOverlaySession';
184 }
185 private function getTokenAuthFromHeader() : ?string
186 {
187 if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer ') === 0) {
188 return substr($_SERVER['HTTP_AUTHORIZATION'], 7);
189 }
190 return null;
191 }
192 private function getTokenAuthFromJsonRequestBody() : ?string
193 {
194 if ($this->isJsonRequestBodyTokenLoaded) {
195 return $this->jsonRequestBodyTokenAuth;
196 }
197 $this->isJsonRequestBodyTokenLoaded = \true;
198 $this->jsonRequestBodyTokenAuth = null;
199 // Token in JSON request body is only supported for tracking requests
200 if (!SettingsServer::isTrackerApiRequest()) {
201 return null;
202 }
203 $requestBody = file_get_contents('php://input');
204 if (!empty($requestBody) && strpos($requestBody, '{') === 0) {
205 $jsonContent = json_decode($requestBody, \true);
206 if (is_array($jsonContent) && !empty($jsonContent['token_auth']) && is_string($jsonContent['token_auth'])) {
207 $this->jsonRequestBodyTokenAuth = $jsonContent['token_auth'];
208 }
209 }
210 return $this->jsonRequestBodyTokenAuth;
211 }
212 }
213