HttpTestTrait - Allow one to easily add Authx JWTs to each request
[civicrm-core.git] / ext / authx / tests / phpunit / Civi / Authx / AllFlowsTest.php
1 <?php
2
3 namespace Civi\Authx;
4
5 use Civi\Test\HttpTestTrait;
6 use CRM_Authx_ExtensionUtil as E;
7 use Civi\Test\EndToEndInterface;
8 use GuzzleHttp\Cookie\CookieJar;
9 use GuzzleHttp\Psr7\AppendStream;
10 use GuzzleHttp\Psr7\Request;
11 use GuzzleHttp\Psr7\Uri;
12 use Psr\Http\Message\ResponseInterface;
13 use function GuzzleHttp\Psr7\stream_for;
14
15 /**
16 * This is a matrix-style test which assesses all supported permutations of
17 *
18 * @group e2e
19 */
20 class AllFlowsTest extends \PHPUnit\Framework\TestCase implements EndToEndInterface {
21
22 use HttpTestTrait;
23
24 /**
25 * Backup copy of the original settings.
26 *
27 * @var array
28 */
29 protected $settingsBackup;
30
31 /**
32 * List of CMS-dependent quirks that should be ignored during testing.
33 * @var array
34 */
35 protected $quirks = [];
36
37 public static function setUpBeforeClass(): void {
38 \Civi\Test::e2e()
39 ->installMe(__DIR__)
40 ->callback(
41 function() {
42 \CRM_Utils_System::synchronizeUsers();
43 },
44 'synchronizeUsers'
45 )
46 ->apply();
47 }
48
49 public function setUp(): void {
50 $quirks = [
51 'Joomla' => ['sendsExcessCookies', 'authErrorShowsForm'],
52 'WordPress' => ['sendsExcessCookies'],
53 ];
54 $this->quirks = $quirks[CIVICRM_UF] ?? [];
55
56 parent::setUp();
57 $this->settingsBackup = [];
58 foreach (\Civi\Authx\Meta::getFlowTypes() as $flowType) {
59 foreach (["authx_{$flowType}_cred", "authx_{$flowType}_user", "authx_guards"] as $setting) {
60 $this->settingsBackup[$setting] = \Civi::settings()->get($setting);
61 }
62 }
63
64 \Civi::settings()->set('authx_guards', []);
65 }
66
67 public function tearDown(): void {
68 foreach ($this->settingsBackup as $setting => $value) {
69 \Civi::settings()->set($setting, $value);
70 }
71 parent::tearDown();
72 }
73
74 public function getStatelessExamples() {
75 $exs = [];
76 $exs[] = ['pass', 'param'];
77 $exs[] = ['pass', 'header'];
78 $exs[] = ['pass', 'xheader'];
79 $exs[] = ['api_key', 'param'];
80 $exs[] = ['api_key', 'header'];
81 $exs[] = ['api_key', 'xheader'];
82 $exs[] = ['jwt', 'param'];
83 $exs[] = ['jwt', 'header'];
84 $exs[] = ['jwt', 'xheader'];
85 return $exs;
86 }
87
88 public function getCredTypes() {
89 $exs = [];
90 $exs[] = ['pass'];
91 $exs[] = ['api_key'];
92 $exs[] = ['jwt'];
93 return $exs;
94 }
95
96 public function testAnonymous(): void {
97 $http = $this->createGuzzle(['http_errors' => FALSE]);
98
99 /** @var \Psr\Http\Message\RequestInterface $request */
100 $request = $this->requestMyContact();
101 $response = $http->send($request);
102 $this->assertAnonymousContact($response);
103 }
104
105 /**
106 * Send a request using a stateless protocol. Assert that identities are setup correctly.
107 *
108 * @param string $credType
109 * The type of credential to put in the `Authorization:` header.
110 * @param string $flowType
111 * The "flow" determines how the credential is added on top of the base-request (e.g. adding a parameter or header).
112 * @throws \CiviCRM_API3_Exception
113 * @throws \GuzzleHttp\Exception\GuzzleException
114 * @dataProvider getStatelessExamples
115 */
116 public function testStatelessContactOnly($credType, $flowType): void {
117 if ($credType === 'pass') {
118 $this->assertTrue(TRUE, 'No need to test password credentials with non-user contacts');
119 return;
120 }
121 $http = $this->createGuzzle(['http_errors' => FALSE]);
122
123 /** @var \Psr\Http\Message\RequestInterface $request */
124 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getLebowskiCID());
125
126 // Phase 1: Request fails if this credential type is not enabled
127 \Civi::settings()->set("authx_{$flowType}_cred", []);
128 $response = $http->send($request);
129 $this->assertFailedDueToProhibition($response);
130
131 // Phase 2: Request succeeds if this credential type is enabled
132 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
133 $response = $http->send($request);
134 $this->assertMyContact($this->getLebowskiCID(), NULL, $credType, $flowType, $response);
135 if (!in_array('sendsExcessCookies', $this->quirks)) {
136 $this->assertNoCookies($response);
137 }
138 }
139
140 /**
141 * Send a request using a stateless protocol. Assert that identities are setup correctly.
142 *
143 * @param string $credType
144 * The type of credential to put in the `Authorization:` header.
145 * @param string $flowType
146 * The "flow" determines how the credential is added on top of the base-request (e.g. adding a parameter or header).
147 * @throws \CiviCRM_API3_Exception
148 * @throws \GuzzleHttp\Exception\GuzzleException
149 * @dataProvider getStatelessExamples
150 */
151 public function testStatelessUserContact($credType, $flowType): void {
152 $http = $this->createGuzzle(['http_errors' => FALSE]);
153
154 /** @var \Psr\Http\Message\RequestInterface $request */
155 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
156
157 // Phase 1: Request fails if this credential type is not enabled
158 \Civi::settings()->set("authx_{$flowType}_cred", []);
159 $response = $http->send($request);
160 $this->assertFailedDueToProhibition($response);
161
162 // Phase 2: Request succeeds if this credential type is enabled
163 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
164 $response = $http->send($request);
165 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
166 if (!in_array('sendsExcessCookies', $this->quirks)) {
167 $this->assertNoCookies($response);
168 }
169 }
170
171 /**
172 * The setting "authx_guard" may be used to require (or not require) the site_key.
173 *
174 * @throws \CiviCRM_API3_Exception
175 * @throws \GuzzleHttp\Exception\GuzzleException
176 */
177 public function testStatelessGuardSiteKey() {
178 if (!defined('CIVICRM_SITE_KEY')) {
179 $this->markTestIncomplete("Cannot run test without CIVICRM_SITE_KEY");
180 }
181
182 $addParam = function($request, $key, $value) {
183 $query = $request->getUri()->getQuery();
184 return $request->withUri(
185 $request->getUri()->withQuery($query . '&' . urlencode($key) . '=' . urlencode($value))
186 );
187 };
188
189 [$credType, $flowType] = ['pass', 'header'];
190 $http = $this->createGuzzle(['http_errors' => FALSE]);
191 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
192
193 /** @var \Psr\Http\Message\RequestInterface $request */
194 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
195
196 // Request OK. Policy requires site_key, and we have one.
197 \Civi::settings()->set("authx_guards", ['site_key']);
198 $response = $http->send($request->withHeader('X-Civi-Key', CIVICRM_SITE_KEY));
199 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
200
201 // Request OK. Policy does not require site_key, and we do not have one
202 \Civi::settings()->set("authx_guards", []);
203 $response = $http->send($request);
204 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
205
206 // Request fails. Policy requires site_key, but we don't have the wrong value.
207 \Civi::settings()->set("authx_guards", ['site_key']);
208 $response = $http->send($request->withHeader('X-Civi-Key', 'not-the-site-key'));
209 $this->assertFailedDueToProhibition($response);
210
211 // Request fails. Policy requires site_key, but we don't have one.
212 \Civi::settings()->set("authx_guards", ['site_key']);
213 $response = $http->send($request);
214 $this->assertFailedDueToProhibition($response);
215 }
216
217 /**
218 * The login flow allows you use 'civicrm/authx/login' and 'civicrm/authx/logout'
219 * to setup/teardown a session.
220 *
221 * @param string $credType
222 * The type of credential to put in the login request.
223 * @throws \CiviCRM_API3_Exception
224 * @throws \GuzzleHttp\Exception\GuzzleException
225 * @dataProvider getCredTypes
226 */
227 public function testStatefulLoginAllowed($credType): void {
228 $flowType = 'login';
229 $credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
230
231 // Phase 1: Some pages are not accessible.
232 $http = $this->createGuzzle(['http_errors' => FALSE]);
233 $http->get('civicrm/user');
234 $this->assertDashboardUnauthorized();
235
236 // Phase 2: Request succeeds if this credential type is enabled
237 $cookieJar = new CookieJar();
238 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
239 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
240 $response = $http->post('civicrm/authx/login', [
241 'form_params' => ['_authx' => $this->$credFunc($this->getDemoCID())],
242 ]);
243 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
244 $this->assertHasCookies($response);
245
246 // Phase 3: We can use cookies to request other pages
247 $response = $http->get('civicrm/authx/id');
248 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
249 $response = $http->get('civicrm/user');
250 $this->assertDashboardOk();
251
252 // Phase 4: After logout, requests should fail.
253 $oldCookies = clone $cookieJar;
254 $http->get('civicrm/authx/logout');
255 $this->assertStatusCode(200);
256 $http->get('civicrm/user');
257 $this->assertDashboardUnauthorized();
258
259 $httpHaxor = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $oldCookies]);
260 $httpHaxor->get('civicrm/user');
261 $this->assertDashboardUnauthorized();
262 }
263
264 /**
265 * The login flow 'civicrm/authx/login' may be prohibited by policy.
266 *
267 * @param string $credType
268 * The type of credential to put in the login request.
269 * @throws \CiviCRM_API3_Exception
270 * @throws \GuzzleHttp\Exception\GuzzleException
271 * @dataProvider getCredTypes
272 */
273 public function testStatefulLoginProhibited($credType): void {
274 $flowType = 'login';
275 $http = $this->createGuzzle(['http_errors' => FALSE]);
276 $credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
277
278 \Civi::settings()->set("authx_{$flowType}_cred", []);
279 $response = $http->post('civicrm/authx/login', [
280 'form_params' => ['_authx' => $this->$credFunc($this->getDemoCID())],
281 ]);
282 $this->assertFailedDueToProhibition($response);
283 }
284
285 /**
286 * The auto-login flow allows you to request a specific page with specific
287 * credentials. The new session is setup, and the page is displayed.
288 *
289 * @param string $credType
290 * The type of credential to put in the login request.
291 * @throws \CiviCRM_API3_Exception
292 * @throws \GuzzleHttp\Exception\GuzzleException
293 * @dataProvider getCredTypes
294 */
295 public function testStatefulAutoAllowed($credType): void {
296 $flowType = 'auto';
297 $cookieJar = new CookieJar();
298 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
299
300 /** @var \Psr\Http\Message\RequestInterface $request */
301 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
302
303 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
304 $this->assertEquals(0, $cookieJar->count());
305 $response = $http->send($request);
306 $this->assertTrue($cookieJar->count() >= 1);
307 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
308
309 // FIXME: Assert that re-using cookies yields correct result.
310 }
311
312 /**
313 * The auto-login flow allows you to request a specific page with specific
314 * credentials. The new session is setup, and the page is displayed.
315 *
316 * @param string $credType
317 * The type of credential to put in the login request.
318 * @throws \CiviCRM_API3_Exception
319 * @throws \GuzzleHttp\Exception\GuzzleException
320 * @dataProvider getCredTypes
321 */
322 public function testStatefulAutoProhibited($credType): void {
323 $flowType = 'auto';
324 $cookieJar = new CookieJar();
325 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
326
327 /** @var \Psr\Http\Message\RequestInterface $request */
328 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
329
330 \Civi::settings()->set("authx_{$flowType}_cred", []);
331 $response = $http->send($request);
332 $this->assertFailedDueToProhibition($response);
333 }
334
335 /**
336 * Create a session for $demoCID. Within the session, make a single
337 * stateless request as $lebowskiCID.
338 *
339 * @throws \CiviCRM_API3_Exception
340 * @throws \GuzzleHttp\Exception\GuzzleException
341 */
342 public function testStatefulStatelessOverlap(): void {
343 \Civi::settings()->set("authx_login_cred", ['api_key']);
344 \Civi::settings()->set("authx_header_cred", ['api_key']);
345
346 $cookieJar = new CookieJar();
347 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
348
349 // Phase 1: Login, create a session.
350 $response = $http->post('civicrm/authx/login', [
351 'form_params' => ['_authx' => $this->credApikey($this->getDemoCID())],
352 ]);
353 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'login', $response);
354 $this->assertHasCookies($response);
355 $response = $http->get('civicrm/authx/id');
356 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'login', $response);
357
358 // Phase 2: Make a single, stateless request with different creds
359 /** @var \Psr\Http\Message\RequestInterface $request */
360 $request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getLebowskiCID());
361 $response = $http->send($request);
362 $this->assertFailedDueToProhibition($response);
363 // The following assertion merely identifies current behavior. If you can get it working generally, then huzza.
364 $this->assertBodyRegexp(';Session already active;', $response);
365 // $this->assertMyContact($this->getLebowskiCID(), NULL, $response);
366 // $this->assertNoCookies($response);
367
368 // Phase 3: Original session is still valid
369 $response = $http->get('civicrm/authx/id');
370 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'login', $response);
371 }
372
373 /**
374 * This consumer intends to make stateless requests with a handful of different identities,
375 * but their browser happens to be cookie-enabled. Ensure that identities do not leak between requests.
376 *
377 * @throws \CiviCRM_API3_Exception
378 * @throws \GuzzleHttp\Exception\GuzzleException
379 */
380 public function testMultipleStateless(): void {
381 \Civi::settings()->set("authx_header_cred", ['api_key']);
382 $cookieJar = new CookieJar();
383 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
384
385 /** @var \Psr\Http\Message\RequestInterface $request */
386
387 // Alternate calls among (A)nonymous, (D)emo, and (L)ebowski
388 $planSteps = 'LADA LDLD DDLLAA';
389 $actualSteps = '';
390
391 for ($i = 0; $i < strlen($planSteps); $i++) {
392 switch ($planSteps[$i]) {
393 case 'L':
394 $request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getLebowskiCID());
395 $response = $http->send($request);
396 $this->assertMyContact($this->getLebowskiCID(), NULL, 'api_key', 'header', $response, 'Expected Lebowski in step #' . $i);
397 $actualSteps .= 'L';
398 break;
399
400 case 'A':
401 $request = $this->requestMyContact();
402 $response = $http->send($request);
403 $this->assertAnonymousContact($response);
404 $actualSteps .= 'A';
405 break;
406
407 case 'D':
408 $request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getDemoCID());
409 $response = $http->send($request);
410 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'header', $response, 'Expected demo in step #' . $i);
411 $actualSteps .= 'D';
412 break;
413
414 case ' ':
415 $actualSteps .= ' ';
416 break;
417
418 default:
419 $this->fail('Unrecognized step #' . $i);
420 }
421 }
422
423 $this->assertEquals($actualSteps, $planSteps);
424 }
425
426 /**
427 * Civi's test suite includes middleware that will add JWT tokens to outgoing requests.
428 *
429 * This test tries a few permutations with different principals ("demo", "Lebowski"),
430 * different identifier fields (authx_user, authx_contact_id), and different
431 * flows (param/header/xheader).
432 *
433 * @throws \CiviCRM_API3_Exception
434 * @throws \GuzzleHttp\Exception\GuzzleException
435 */
436 public function testJwtMiddleware() {
437 // HTTP GET with a specific user. Choose flow automatically.
438 $response = $this->createGuzzle()->get('civicrm/authx/id', [
439 'authx_user' => $GLOBALS['_CV']['DEMO_USER'],
440 ]);
441 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'jwt', 'param', $response);
442
443 // HTTP GET with a specific contact. Choose flow automatically.
444 $response = $this->createGuzzle()->get('civicrm/authx/id', [
445 'authx_contact_id' => $this->getDemoCID(),
446 ]);
447 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'jwt', 'param', $response);
448
449 // HTTP POST with a specific contact. Per-client default.
450 $response = $this->createGuzzle([
451 'authx_contact_id' => $this->getLebowskiCID(),
452 ])->post('civicrm/authx/id');
453 $this->assertMyContact($this->getLebowskiCID(), NULL, 'jwt', 'param', $response);
454
455 // Using explicit flow options...
456 foreach (['param', 'xheader', 'header'] as $flowType) {
457 \Civi::settings()->set("authx_{$flowType}_cred", ['jwt']);
458 $response = $this->createGuzzle()->get('civicrm/authx/id', [
459 'authx_contact_id' => $this->getDemoCID(),
460 'authx_flow' => $flowType,
461 ]);
462 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'jwt', $flowType, $response);
463 }
464 }
465
466 /**
467 * Filter a request, applying the given authentication options
468 *
469 * @param \Psr\Http\Message\RequestInterface $request
470 * @param string $credType
471 * Ex: 'pass', 'jwt', 'api_key'
472 * @param string $flowType
473 * Ex: 'param', 'header', 'xheader'
474 * @param int $cid
475 * @return \Psr\Http\Message\RequestInterface
476 */
477 protected function applyAuth($request, $credType, $flowType, $cid) {
478 $credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
479 $flowFunc = 'auth' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $flowType));
480 return $this->$flowFunc($request, $this->$credFunc($cid));
481 }
482
483 // ------------------------------------------------
484 // Library: Base requests
485
486 /**
487 * Make an AJAX request with info about the current contact.
488 *
489 * @return \GuzzleHttp\Psr7\Request
490 */
491 public function requestMyContact() {
492 $p = (['where' => [['id', '=', 'user_contact_id']]]);
493 $uri = (new Uri('civicrm/authx/id'))
494 ->withQuery('params=' . urlencode(json_encode($p)));
495 $req = new Request('GET', $uri);
496 return $req;
497 }
498
499 /**
500 * Assert the AJAX request provided the expected contact.
501 *
502 * @param int $cid
503 * The expected contact ID
504 * @param int|null $uid
505 * The expected user ID
506 * @param string $credType
507 * @param string $flow
508 * @param \Psr\Http\Message\ResponseInterface $response
509 */
510 public function assertMyContact($cid, $uid, $credType, $flow, ResponseInterface $response): void {
511 $this->assertContentType('application/json', $response);
512 $this->assertStatusCode(200, $response);
513 $j = json_decode((string) $response->getBody(), 1);
514 $formattedFailure = $this->formatFailure($response);
515 $this->assertEquals($cid, $j['contact_id'], "Response did not give expected contact ID\n" . $formattedFailure);
516 $this->assertEquals($uid, $j['user_id'], "Response did not give expected user ID\n" . $formattedFailure);
517 if ($flow !== NULL) {
518 $this->assertEquals($flow, $j['flow'], "Response did not give expected flow type\n" . $formattedFailure);
519 }
520 if ($credType !== NULL) {
521 $this->assertEquals($credType, $j['cred'], "Response did not give expected cred type\n" . $formattedFailure);
522 }
523 }
524
525 /**
526 * Assert the AJAX request provided empty contact information
527 *
528 * @param \Psr\Http\Message\ResponseInterface $response
529 */
530 public function assertAnonymousContact(ResponseInterface $response): void {
531 $formattedFailure = $this->formatFailure($response);
532 $this->assertContentType('application/json', $response);
533 $this->assertStatusCode(200, $response);
534 $j = json_decode((string) $response->getBody(), 1);
535 if (json_last_error() !== JSON_ERROR_NONE || empty($j)) {
536 $this->fail('Malformed JSON' . $formattedFailure);
537 }
538 $this->assertTrue(array_key_exists('contact_id', $j) && $j['contact_id'] === NULL, 'contact_id should be null' . $formattedFailure);
539 $this->assertTrue(array_key_exists('user_id', $j) && $j['user_id'] === NULL, 'user_id should be null' . $formattedFailure);
540 }
541
542 /**
543 * Assert that the $response indicates the user cannot view the dashboard.
544 *
545 * @param \Psr\Http\Message\ResponseInterface $response
546 */
547 public function assertDashboardUnauthorized($response = NULL): void {
548 $response = $this->resolveResponse($response);
549 if (!in_array('authErrorShowsForm', $this->quirks)) {
550 $this->assertStatusCode(403, $response);
551 }
552 $this->assertFalse(
553 (bool) preg_match(';crm-dashboard-groups;', (string) $response->getBody()),
554 'Response should not contain a dashboard' . $this->formatFailure($response)
555 );
556 }
557
558 public function assertDashboardOk($response = NULL): void {
559 $response = $this->resolveResponse($response);
560 $this->assertStatusCode(200, $response);
561 $this->assertContentType('text/html', $response);
562 // If the first two assertions pass but the next fails, then... perhaps the
563 // local site permissions are wrong?
564 $this->assertTrue(
565 (bool) preg_match(';crm-dashboard-groups;', (string) $response->getBody()),
566 'Response should contain a dashboard' . $this->formatFailure($response)
567 );
568 }
569
570 // ------------------------------------------------
571 // Library: Flow functions
572
573 /**
574 * Add query parameter ("&_authx=<CRED>").
575 *
576 * @param \GuzzleHttp\Psr7\Request $request
577 * @param string $cred
578 * The credential add to the request (e.g. "Basic ASDF==" or "Bearer FDSA").
579 * @return \GuzzleHttp\Psr7\Request
580 */
581 public function authParam(Request $request, $cred) {
582 $query = $request->getUri()->getQuery();
583 return $request->withUri(
584 $request->getUri()->withQuery($query . '&_authx=' . urlencode($cred))
585 );
586 }
587
588 /**
589 * Add query parameter ("&_authx=<CRED>&_authxSes=1").
590 *
591 * @param \GuzzleHttp\Psr7\Request $request
592 * @param string $cred
593 * The credential add to the request (e.g. "Basic ASDF==" or "Bearer FDSA").
594 * @return \GuzzleHttp\Psr7\Request
595 */
596 public function authAuto(Request $request, $cred) {
597 $query = $request->getUri()->getQuery();
598 return $request->withUri(
599 $request->getUri()->withQuery($query . '&_authx=' . urlencode($cred) . '&_authxSes=1')
600 );
601 }
602
603 public function authLogin(Request $request, $cred) {
604 return $request->withMethod('POST')
605 ->withBody(new AppendStream([
606 stream_for('_authx=' . urlencode($cred) . '&'),
607 $request->getBody(),
608 ]));
609 }
610
611 public function authHeader(Request $request, $cred) {
612 return $request->withHeader('Authorization', $cred);
613 }
614
615 public function authXHeader(Request $request, $cred) {
616 return $request->withHeader('X-Civi-Auth', $cred);
617 }
618
619 public function authNone(Request $request, $cred) {
620 return $request;
621 }
622
623 // ------------------------------------------------
624 // Library: Credential functions
625
626 /**
627 * @param int $cid
628 * @return string
629 * The credential add to the request (e.g. "Basic ASDF==" or "Bearer FDSA").
630 */
631 public function credPass($cid) {
632 if ($cid === $this->getDemoCID()) {
633 return 'Basic ' . base64_encode($GLOBALS['_CV']['DEMO_USER'] . ':' . $GLOBALS['_CV']['DEMO_PASS']);
634 }
635 else {
636 $this->fail("This test does have the password the requested contact.");
637 }
638 }
639
640 public function credApikey($cid) {
641 $api_key = md5(\random_bytes(16));
642 \civicrm_api3('Contact', 'create', [
643 'id' => $cid,
644 'api_key' => $api_key,
645 ]);
646 return 'Bearer ' . $api_key;
647 }
648
649 public function credJwt($cid) {
650 if (empty(\Civi::service('crypto.registry')->findKeysByTag('SIGN'))) {
651 $this->markTestIncomplete('Cannot test JWT. No CIVICRM_SIGN_KEYS are defined.');
652 }
653 $token = \Civi::service('crypto.jwt')->encode([
654 'exp' => time() + 60 * 60,
655 'sub' => "cid:$cid",
656 'scope' => 'authx',
657 ]);
658 return 'Bearer ' . $token;
659 }
660
661 public function credNone($cid) {
662 return NULL;
663 }
664
665 /**
666 * @param \Psr\Http\Message\ResponseInterface $response
667 */
668 private function assertFailedDueToProhibition($response): void {
669 $this->assertBodyRegexp(';HTTP 401;', $response);
670 $this->assertContentType('text/plain', $response);
671 if (!in_array('sendsExcessCookies', $this->quirks)) {
672 $this->assertNoCookies($response);
673 }
674 $this->assertStatusCode(401, $response);
675
676 }
677
678 /**
679 * @param \Psr\Http\Message\ResponseInterface $response
680 */
681 private function assertNoCookies($response = NULL) {
682 $response = $this->resolveResponse($response);
683 $this->assertEmpty(
684 preg_grep('/Set-Cookie/i', array_keys($response->getHeaders())),
685 'Response should not have cookies' . $this->formatFailure($response)
686 );
687 return $this;
688 }
689
690 /**
691 * @param \Psr\Http\Message\ResponseInterface $response
692 */
693 private function assertHasCookies($response = NULL) {
694 $response = $this->resolveResponse($response);
695 $this->assertNotEmpty(
696 preg_grep('/Set-Cookie/i', array_keys($response->getHeaders())),
697 'Response should have cookies' . $this->formatFailure($response)
698 );
699 return $this;
700 }
701
702 /**
703 * @param $regexp
704 * @param \Psr\Http\Message\ResponseInterface $response
705 */
706 private function assertBodyRegexp($regexp, $response = NULL) {
707 $response = $this->resolveResponse($response);
708 $this->assertRegexp($regexp, (string) $response->getBody(),
709 'Response body does not match pattern' . $this->formatFailure($response));
710 return $this;
711 }
712
713 /**
714 * @return int
715 * @throws \CiviCRM_API3_Exception
716 */
717 private function getDemoCID(): int {
718 if (!isset(\Civi::$statics[__CLASS__]['demoId'])) {
719 \Civi::$statics[__CLASS__]['demoId'] = (int) \civicrm_api3('Contact', 'getvalue', [
720 'id' => '@user:' . $GLOBALS['_CV']['DEMO_USER'],
721 'return' => 'id',
722 ]);
723 }
724 return \Civi::$statics[__CLASS__]['demoId'];
725 }
726
727 private function getDemoUID(): int {
728 return \CRM_Core_Config::singleton()->userSystem->getUfId($GLOBALS['_CV']['DEMO_USER']);
729 }
730
731 public function getLebowskiCID() {
732 if (!isset(\Civi::$statics[__CLASS__]['lebowskiCID'])) {
733 $contact = \civicrm_api3('Contact', 'create', [
734 'contact_type' => 'Individual',
735 'first_name' => 'Jeffrey',
736 'last_name' => 'Lebowski',
737 'external_identifier' => __CLASS__,
738 'options' => [
739 'match' => 'external_identifier',
740 ],
741 ]);
742 \Civi::$statics[__CLASS__]['lebowskiCID'] = $contact['id'];
743 }
744 return \Civi::$statics[__CLASS__]['lebowskiCID'];
745 }
746
747 }