$tgt = AuthenticatorTarget::create([
'flow' => $details['flow'],
'cred' => $details['cred'],
+ 'siteKey' => $details['siteKey'] ?? NULL,
'useSession' => $details['useSession'] ?? FALSE,
]);
if ($principal = $this->checkCredential($tgt)) {
}
break;
}
+
+ $useGuards = \Civi::settings()->get('authx_guards');
+ if (!empty($useGuards)) {
+ // array(string $credType => string $requiredPermissionToUseThisCred)
+ $perms['pass'] = 'authenticate with password';
+ $perms['api_key'] = 'authenticate with api key';
+
+ // If any one of these passes, then we allow the authentication.
+ $passGuard = [];
+ $passGuard[] = in_array('site_key', $useGuards) && defined('CIVICRM_SITE_KEY') && hash_equals(CIVICRM_SITE_KEY, $tgt->siteKey);
+ $passGuard[] = in_array('perm', $useGuards) && isset($perms[$tgt->credType]) && \CRM_Core_Permission::check($perms[$tgt->credType], $tgt->contactId);
+ // JWTs are signed by us. We don't need user to prove that they're allowed to use them.
+ $passGuard[] = ($tgt->credType === 'jwt');
+ if (!max($passGuard)) {
+ $this->reject(sprintf('Login not permitted. Must satisfy guard (%s).', implode(', ', $useGuards)));
+ }
+ }
}
/**
* @param string $message
*/
protected function reject($message = 'Authentication failed') {
+ \CRM_Core_Session::useFakeSession();
$r = new Response(401, ['Content-Type' => 'text/plain'], "HTTP 401 $message");
\CRM_Utils_System::sendResponse($r);
}
*/
public $cred;
+ /**
+ * The raw site-key as submitted (if applicable).
+ * @var string
+ */
+ public $siteKey;
+
/**
* (Authenticated) The type of credential.
*
// phpcs:enable
Civi::dispatcher()->addListener('civi.invoke.auth', function($e) {
+ $params = ($_SERVER['REQUEST_METHOD'] === 'GET') ? $_GET : $_POST;
+ $siteKey = $_SERVER['HTTP_X_CIVI_KEY'] ?? $params['_authxSiteKey'] ?? NULL;
+
if (!empty($_SERVER['HTTP_X_CIVI_AUTH'])) {
- return (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'xheader', 'cred' => $_SERVER['HTTP_X_CIVI_AUTH']]);
+ return (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'xheader', 'cred' => $_SERVER['HTTP_X_CIVI_AUTH'], 'siteKey' => $siteKey]);
}
if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
- return (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'header', 'cred' => $_SERVER['HTTP_AUTHORIZATION']]);
+ return (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'header', 'cred' => $_SERVER['HTTP_AUTHORIZATION'], 'siteKey' => $siteKey]);
}
- $params = ($_SERVER['REQUEST_METHOD'] === 'GET') ? $_GET : $_POST;
if (!empty($params['_authx'])) {
if ((implode('/', $e->args) === 'civicrm/authx/login')) {
- (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'login', 'cred' => $params['_authx'], 'useSession' => TRUE]);
+ (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'login', 'cred' => $params['_authx'], 'useSession' => TRUE, 'siteKey' => $siteKey]);
_authx_redact(['_authx']);
}
elseif (!empty($params['_authxSes'])) {
- (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'auto', 'cred' => $params['_authx'], 'useSession' => TRUE]);
+ (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'auto', 'cred' => $params['_authx'], 'useSession' => TRUE, 'siteKey' => $siteKey]);
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
_authx_reload(implode('/', $e->args), $_SERVER['QUERY_STRING']);
}
}
}
else {
- (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'param', 'cred' => $params['_authx']]);
+ (new \Civi\Authx\Authenticator())->auth($e, ['flow' => 'param', 'cred' => $params['_authx'], 'siteKey' => $siteKey]);
_authx_redact(['_authx']);
}
}
_authx_civix_civicrm_themes($themes);
}
+/**
+ * Implements hook_civicrm_permission().
+ *
+ * @see CRM_Utils_Hook::permission()
+ */
+function authx_civicrm_permission(&$permissions) {
+ $permissions['authenticate with password'] = ts('AuthX: Authenticate to services with password');
+ $permissions['authenticate with api key'] = ts('AuthX: Authenticate to services with API key');
+}
+
// --- Functions below this ship commented out. Uncomment as required. ---
/**
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() {
}
}
+ /**
+ * 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(), $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(), $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.