*/
protected $quirks = [];
- public static function setUpBeforeClass() {
+ public static function setUpBeforeClass(): void {
\Civi\Test::e2e()
->installMe(__DIR__)
->callback(
->apply();
}
- public function setUp() {
+ public function setUp(): void {
$quirks = [
'Joomla' => ['sendsExcessCookies', 'authErrorShowsForm'],
'WordPress' => ['sendsExcessCookies'],
parent::setUp();
$this->settingsBackup = [];
foreach (\Civi\Authx\Meta::getFlowTypes() as $flowType) {
- foreach (["authx_{$flowType}_cred", "authx_{$flowType}_user"] as $setting) {
+ foreach (["authx_{$flowType}_cred", "authx_{$flowType}_user", "authx_guards"] as $setting) {
$this->settingsBackup[$setting] = \Civi::settings()->get($setting);
}
}
+
+ \Civi::settings()->set('authx_guards', []);
}
- public function tearDown() {
+ public function tearDown(): void {
foreach ($this->settingsBackup as $setting => $value) {
\Civi::settings()->set($setting, $value);
}
return $exs;
}
- public function testAnonymous() {
+ public function testAnonymous(): void {
$http = $this->createGuzzle(['http_errors' => FALSE]);
/** @var \Psr\Http\Message\RequestInterface $request */
* @throws \GuzzleHttp\Exception\GuzzleException
* @dataProvider getStatelessExamples
*/
- public function testStatelessContactOnly($credType, $flowType) {
+ public function testStatelessContactOnly($credType, $flowType): void {
if ($credType === 'pass') {
$this->assertTrue(TRUE, 'No need to test password credentials with non-user contacts');
return;
// Phase 2: Request succeeds if this credential type is enabled
\Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
$response = $http->send($request);
- $this->assertMyContact($this->getLebowskiCID(), NULL, $response);
+ $this->assertMyContact($this->getLebowskiCID(), NULL, $credType, $flowType, $response);
if (!in_array('sendsExcessCookies', $this->quirks)) {
$this->assertNoCookies($response);
}
* @throws \GuzzleHttp\Exception\GuzzleException
* @dataProvider getStatelessExamples
*/
- public function testStatelessUserContact($credType, $flowType) {
+ public function testStatelessUserContact($credType, $flowType): void {
$http = $this->createGuzzle(['http_errors' => FALSE]);
/** @var \Psr\Http\Message\RequestInterface $request */
// Phase 2: Request succeeds if this credential type is enabled
\Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
$response = $http->send($request);
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
if (!in_array('sendsExcessCookies', $this->quirks)) {
$this->assertNoCookies($response);
}
}
+ /**
+ * The setting "authx_guard" may be used to require (or not require) the site_key.
+ *
+ * @throws \CiviCRM_API3_Exception
+ * @throws \GuzzleHttp\Exception\GuzzleException
+ */
+ public function testStatelessGuardSiteKey() {
+ if (!defined('CIVICRM_SITE_KEY')) {
+ $this->markTestIncomplete("Cannot run test without CIVICRM_SITE_KEY");
+ }
+
+ $addParam = function($request, $key, $value) {
+ $query = $request->getUri()->getQuery();
+ return $request->withUri(
+ $request->getUri()->withQuery($query . '&' . urlencode($key) . '=' . urlencode($value))
+ );
+ };
+
+ [$credType, $flowType] = ['pass', 'header'];
+ $http = $this->createGuzzle(['http_errors' => FALSE]);
+ \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
+
+ /** @var \Psr\Http\Message\RequestInterface $request */
+ $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
+
+ // Request OK. Policy requires site_key, and we have one.
+ \Civi::settings()->set("authx_guards", ['site_key']);
+ $response = $http->send($request->withHeader('X-Civi-Key', CIVICRM_SITE_KEY));
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
+
+ // Request OK. Policy does not require site_key, and we do not have one
+ \Civi::settings()->set("authx_guards", []);
+ $response = $http->send($request);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
+
+ // Request fails. Policy requires site_key, but we don't have the wrong value.
+ \Civi::settings()->set("authx_guards", ['site_key']);
+ $response = $http->send($request->withHeader('X-Civi-Key', 'not-the-site-key'));
+ $this->assertFailedDueToProhibition($response);
+
+ // Request fails. Policy requires site_key, but we don't have one.
+ \Civi::settings()->set("authx_guards", ['site_key']);
+ $response = $http->send($request);
+ $this->assertFailedDueToProhibition($response);
+ }
+
/**
* The login flow allows you use 'civicrm/authx/login' and 'civicrm/authx/logout'
* to setup/teardown a session.
* @throws \GuzzleHttp\Exception\GuzzleException
* @dataProvider getCredTypes
*/
- public function testStatefulLoginAllowed($credType) {
+ public function testStatefulLoginAllowed($credType): void {
$flowType = 'login';
$credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
$response = $http->post('civicrm/authx/login', [
'form_params' => ['_authx' => $this->$credFunc($this->getDemoCID())],
]);
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
$this->assertHasCookies($response);
// Phase 3: We can use cookies to request other pages
$response = $http->get('civicrm/authx/id');
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
$response = $http->get('civicrm/user');
$this->assertDashboardOk();
* @throws \GuzzleHttp\Exception\GuzzleException
* @dataProvider getCredTypes
*/
- public function testStatefulLoginProhibited($credType) {
+ public function testStatefulLoginProhibited($credType): void {
$flowType = 'login';
$http = $this->createGuzzle(['http_errors' => FALSE]);
$credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
* @throws \GuzzleHttp\Exception\GuzzleException
* @dataProvider getCredTypes
*/
- public function testStatefulAutoAllowed($credType) {
+ public function testStatefulAutoAllowed($credType): void {
$flowType = 'auto';
$cookieJar = new CookieJar();
$http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
$this->assertEquals(0, $cookieJar->count());
$response = $http->send($request);
$this->assertTrue($cookieJar->count() >= 1);
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $credType, $flowType, $response);
// FIXME: Assert that re-using cookies yields correct result.
}
* @throws \GuzzleHttp\Exception\GuzzleException
* @dataProvider getCredTypes
*/
- public function testStatefulAutoProhibited($credType) {
+ public function testStatefulAutoProhibited($credType): void {
$flowType = 'auto';
$cookieJar = new CookieJar();
$http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
* @throws \CiviCRM_API3_Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
- public function testStatefulStatelessOverlap() {
+ public function testStatefulStatelessOverlap(): void {
\Civi::settings()->set("authx_login_cred", ['api_key']);
\Civi::settings()->set("authx_header_cred", ['api_key']);
$response = $http->post('civicrm/authx/login', [
'form_params' => ['_authx' => $this->credApikey($this->getDemoCID())],
]);
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'login', $response);
$this->assertHasCookies($response);
$response = $http->get('civicrm/authx/id');
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'login', $response);
// Phase 2: Make a single, stateless request with different creds
/** @var \Psr\Http\Message\RequestInterface $request */
// Phase 3: Original session is still valid
$response = $http->get('civicrm/authx/id');
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'login', $response);
}
/**
* @throws \CiviCRM_API3_Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
- public function testMultipleStateless() {
+ public function testMultipleStateless(): void {
\Civi::settings()->set("authx_header_cred", ['api_key']);
$cookieJar = new CookieJar();
$http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
case 'L':
$request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getLebowskiCID());
$response = $http->send($request);
- $this->assertMyContact($this->getLebowskiCID(), NULL, $response, 'Expected Lebowski in step #' . $i);
+ $this->assertMyContact($this->getLebowskiCID(), NULL, 'api_key', 'header', $response, 'Expected Lebowski in step #' . $i);
$actualSteps .= 'L';
break;
case 'D':
$request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getDemoCID());
$response = $http->send($request);
- $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response, 'Expected demo in step #' . $i);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'api_key', 'header', $response, 'Expected demo in step #' . $i);
$actualSteps .= 'D';
break;
$this->assertEquals($actualSteps, $planSteps);
}
+ /**
+ * Civi's test suite includes middleware that will add JWT tokens to outgoing requests.
+ *
+ * This test tries a few permutations with different principals ("demo", "Lebowski"),
+ * different identifier fields (authx_user, authx_contact_id), and different
+ * flows (param/header/xheader).
+ *
+ * @throws \CiviCRM_API3_Exception
+ * @throws \GuzzleHttp\Exception\GuzzleException
+ */
+ public function testJwtMiddleware() {
+ // HTTP GET with a specific user. Choose flow automatically.
+ $response = $this->createGuzzle()->get('civicrm/authx/id', [
+ 'authx_user' => $GLOBALS['_CV']['DEMO_USER'],
+ ]);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'jwt', 'param', $response);
+
+ // HTTP GET with a specific contact. Choose flow automatically.
+ $response = $this->createGuzzle()->get('civicrm/authx/id', [
+ 'authx_contact_id' => $this->getDemoCID(),
+ ]);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'jwt', 'param', $response);
+
+ // HTTP POST with a specific contact. Per-client default.
+ $response = $this->createGuzzle([
+ 'authx_contact_id' => $this->getLebowskiCID(),
+ ])->post('civicrm/authx/id');
+ $this->assertMyContact($this->getLebowskiCID(), NULL, 'jwt', 'param', $response);
+
+ // Using explicit flow options...
+ foreach (['param', 'xheader', 'header'] as $flowType) {
+ \Civi::settings()->set("authx_{$flowType}_cred", ['jwt']);
+ $response = $this->createGuzzle()->get('civicrm/authx/id', [
+ 'authx_contact_id' => $this->getDemoCID(),
+ 'authx_flow' => $flowType,
+ ]);
+ $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), 'jwt', $flowType, $response);
+ }
+ }
+
/**
* Filter a request, applying the given authentication options
*
* The expected contact ID
* @param int|null $uid
* The expected user ID
+ * @param string $credType
+ * @param string $flow
* @param \Psr\Http\Message\ResponseInterface $response
*/
- public function assertMyContact($cid, $uid, ResponseInterface $response) {
+ public function assertMyContact($cid, $uid, $credType, $flow, ResponseInterface $response): void {
$this->assertContentType('application/json', $response);
$this->assertStatusCode(200, $response);
$j = json_decode((string) $response->getBody(), 1);
$formattedFailure = $this->formatFailure($response);
$this->assertEquals($cid, $j['contact_id'], "Response did not give expected contact ID\n" . $formattedFailure);
$this->assertEquals($uid, $j['user_id'], "Response did not give expected user ID\n" . $formattedFailure);
+ if ($flow !== NULL) {
+ $this->assertEquals($flow, $j['flow'], "Response did not give expected flow type\n" . $formattedFailure);
+ }
+ if ($credType !== NULL) {
+ $this->assertEquals($credType, $j['cred'], "Response did not give expected cred type\n" . $formattedFailure);
+ }
}
/**
*
* @param \Psr\Http\Message\ResponseInterface $response
*/
- public function assertAnonymousContact(ResponseInterface $response) {
+ public function assertAnonymousContact(ResponseInterface $response): void {
$formattedFailure = $this->formatFailure($response);
$this->assertContentType('application/json', $response);
$this->assertStatusCode(200, $response);
*
* @param \Psr\Http\Message\ResponseInterface $response
*/
- public function assertDashboardUnauthorized($response = NULL) {
+ public function assertDashboardUnauthorized($response = NULL): void {
$response = $this->resolveResponse($response);
if (!in_array('authErrorShowsForm', $this->quirks)) {
$this->assertStatusCode(403, $response);
);
}
- public function assertDashboardOk($response = NULL) {
+ public function assertDashboardOk($response = NULL): void {
$response = $this->resolveResponse($response);
$this->assertStatusCode(200, $response);
$this->assertContentType('text/html', $response);
/**
* @param \Psr\Http\Message\ResponseInterface $response
*/
- private function assertFailedDueToProhibition($response) {
+ private function assertFailedDueToProhibition($response): void {
$this->assertBodyRegexp(';HTTP 401;', $response);
$this->assertContentType('text/plain', $response);
if (!in_array('sendsExcessCookies', $this->quirks)) {