3 abstract class PhabricatorAuthProvider
extends Phobject
{
5 private $providerConfig;
7 public function attachProviderConfig(PhabricatorAuthProviderConfig
$config) {
8 $this->providerConfig
= $config;
12 public function hasProviderConfig() {
13 return (bool)$this->providerConfig
;
16 public function getProviderConfig() {
17 if ($this->providerConfig
=== null) {
18 throw new PhutilInvalidStateException('attachProviderConfig');
20 return $this->providerConfig
;
23 public function getProviderConfigPHID() {
24 return $this->getProviderConfig()->getPHID();
27 public function getConfigurationHelp() {
31 public function getDefaultProviderConfig() {
32 return id(new PhabricatorAuthProviderConfig())
33 ->setProviderClass(get_class($this))
35 ->setShouldAllowLogin(1)
36 ->setShouldAllowRegistration(1)
37 ->setShouldAllowLink(1)
38 ->setShouldAllowUnlink(1);
41 public function getNameForCreate() {
42 return $this->getProviderName();
45 public function getDescriptionForCreate() {
49 public function getProviderKey() {
50 return $this->getAdapter()->getAdapterKey();
53 public function getProviderType() {
54 return $this->getAdapter()->getAdapterType();
57 public function getProviderDomain() {
58 return $this->getAdapter()->getAdapterDomain();
61 public static function getAllBaseProviders() {
62 return id(new PhutilClassMapQuery())
63 ->setAncestorClass(__CLASS__
)
67 public static function getAllProviders() {
70 if ($providers === null) {
71 $objects = self
::getAllBaseProviders();
73 $configs = id(new PhabricatorAuthProviderConfigQuery())
74 ->setViewer(PhabricatorUser
::getOmnipotentUser())
78 foreach ($configs as $config) {
79 if (!isset($objects[$config->getProviderClass()])) {
80 // This configuration is for a provider which is not installed.
84 $object = clone $objects[$config->getProviderClass()];
85 $object->attachProviderConfig($config);
87 $key = $object->getProviderKey();
88 if (isset($providers[$key])) {
91 "Two authentication providers use the same provider key ".
92 "('%s'). Each provider must be identified by a unique key.",
95 $providers[$key] = $object;
102 public static function getAllEnabledProviders() {
103 $providers = self
::getAllProviders();
104 foreach ($providers as $key => $provider) {
105 if (!$provider->isEnabled()) {
106 unset($providers[$key]);
112 public static function getEnabledProviderByKey($provider_key) {
113 return idx(self
::getAllEnabledProviders(), $provider_key);
116 abstract public function getProviderName();
117 abstract public function getAdapter();
119 public function isEnabled() {
120 return $this->getProviderConfig()->getIsEnabled();
123 public function shouldAllowLogin() {
124 return $this->getProviderConfig()->getShouldAllowLogin();
127 public function shouldAllowRegistration() {
128 if (!$this->shouldAllowLogin()) {
132 return $this->getProviderConfig()->getShouldAllowRegistration();
135 public function shouldAllowAccountLink() {
136 return $this->getProviderConfig()->getShouldAllowLink();
139 public function shouldAllowAccountUnlink() {
140 return $this->getProviderConfig()->getShouldAllowUnlink();
143 public function shouldTrustEmails() {
144 return $this->shouldAllowEmailTrustConfiguration() &&
145 $this->getProviderConfig()->getShouldTrustEmails();
149 * Should we allow the adapter to be marked as "trusted". This is true for
150 * all adapters except those that allow the user to type in emails (see
151 * @{class:PhabricatorPasswordAuthProvider}).
153 public function shouldAllowEmailTrustConfiguration() {
157 public function buildLoginForm(PhabricatorAuthStartController
$controller) {
158 return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
161 public function buildInviteForm(PhabricatorAuthStartController
$controller) {
162 return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
165 abstract public function processLoginRequest(
166 PhabricatorAuthLoginController
$controller);
168 public function buildLinkForm($controller) {
169 return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
172 public function shouldAllowAccountRefresh() {
176 public function buildRefreshForm(
177 PhabricatorAuthLinkController
$controller) {
178 return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
181 protected function renderLoginForm(AphrontRequest
$request, $mode) {
182 throw new PhutilMethodNotImplementedException();
185 public function createProviders() {
189 protected function willSaveAccount(PhabricatorExternalAccount
$account) {
193 final protected function newExternalAccountForIdentifiers(
194 array $identifiers) {
196 assert_instances_of($identifiers, 'PhabricatorExternalAccountIdentifier');
201 'Authentication provider (of class "%s") is attempting to '.
202 'load or create an external account, but provided no account '.
207 $config = $this->getProviderConfig();
208 $viewer = PhabricatorUser
::getOmnipotentUser();
210 $raw_identifiers = mpull($identifiers, 'getIdentifierRaw');
212 $accounts = id(new PhabricatorExternalAccountQuery())
214 ->withProviderConfigPHIDs(array($config->getPHID()))
215 ->withRawAccountIdentifiers($raw_identifiers)
216 ->needAccountIdentifiers(true)
219 $account = $this->newExternalAccount();
220 } else if (count($accounts) === 1) {
221 $account = head($accounts);
225 'Authentication provider (of class "%s") is attempting to load '.
226 'or create an external account, but provided a list of '.
227 'account identifiers which map to more than one account: %s.',
229 implode(', ', $raw_identifiers)));
232 // See T13493. Add all the identifiers to the account. In the case where
233 // an account initially has a lower-quality identifier (like an email
234 // address) and later adds a higher-quality identifier (like a GUID), this
235 // allows us to automatically upgrade toward the higher-quality identifier
236 // and survive API changes which remove the lower-quality identifier more
239 foreach ($identifiers as $identifier) {
240 $account->appendIdentifier($identifier);
243 return $this->didUpdateAccount($account);
246 final protected function newExternalAccountForUser(PhabricatorUser
$user) {
247 $config = $this->getProviderConfig();
249 // When a user logs in with a provider like username/password, they
250 // always already have a Phabricator account (since there's no way they
251 // could have a username otherwise).
253 // These users should never go to registration, so we're building a
254 // dummy "external account" which just links directly back to their
257 $account = id(new PhabricatorExternalAccountQuery())
259 ->withProviderConfigPHIDs(array($config->getPHID()))
260 ->withUserPHIDs(array($user->getPHID()))
263 $account = $this->newExternalAccount()
264 ->setUserPHID($user->getPHID());
267 return $this->didUpdateAccount($account);
270 private function didUpdateAccount(PhabricatorExternalAccount
$account) {
271 $adapter = $this->getAdapter();
273 $account->setUsername($adapter->getAccountName());
274 $account->setRealName($adapter->getAccountRealName());
275 $account->setEmail($adapter->getAccountEmail());
276 $account->setAccountURI($adapter->getAccountURI());
278 $account->setProfileImagePHID(null);
279 $image_uri = $adapter->getAccountImageURI();
282 $name = PhabricatorSlug
::normalize($this->getProviderName());
283 $name = $name.'-profile.jpg';
285 // TODO: If the image has not changed, we do not need to make a new
286 // file entry for it, but there's no convenient way to do this with
287 // PhabricatorFile right now. The storage will get shared, so the impact
288 // here is negligible.
290 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
291 $image_file = PhabricatorFile
::newFromFileDownload(
295 'viewPolicy' => PhabricatorPolicies
::POLICY_NOONE
,
297 if ($image_file->isViewableImage()) {
299 ->setViewPolicy(PhabricatorPolicies
::getMostOpenPolicy())
302 $account->setProfileImagePHID($image_file->getPHID());
304 $image_file->delete();
308 } catch (Exception
$ex) {
309 // Log this but proceed, it's not especially important that we
310 // be able to pull profile images.
315 $this->willSaveAccount($account);
317 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
324 public function getLoginURI() {
325 $app = PhabricatorApplication
::getByClass('PhabricatorAuthApplication');
326 return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
329 public function getSettingsURI() {
330 return '/settings/panel/external/';
333 public function getStartURI() {
334 $app = PhabricatorApplication
::getByClass('PhabricatorAuthApplication');
335 $uri = $app->getApplicationURI('/start/');
339 public function isDefaultRegistrationProvider() {
343 public function shouldRequireRegistrationPassword() {
347 public function newDefaultExternalAccount() {
348 return $this->newExternalAccount();
351 protected function newExternalAccount() {
352 $config = $this->getProviderConfig();
353 $adapter = $this->getAdapter();
355 $account = id(new PhabricatorExternalAccount())
356 ->setProviderConfigPHID($config->getPHID())
357 ->attachAccountIdentifiers(array());
359 // TODO: Remove this when these columns are removed. They no longer have
360 // readers or writers (other than this callsite).
363 ->setAccountType($adapter->getAdapterType())
364 ->setAccountDomain($adapter->getAdapterDomain());
366 // TODO: Remove this when "accountID" is removed; the column is not
369 $account->setAccountID('');
374 public function getLoginOrder() {
375 return '500-'.$this->getProviderName();
378 protected function getLoginIcon() {
382 public function newIconView() {
383 return id(new PHUIIconView())
384 ->setSpriteSheet(PHUIIconView
::SPRITE_LOGIN
)
385 ->setSpriteIcon($this->getLoginIcon());
388 public function isLoginFormAButton() {
392 public function renderConfigPropertyTransactionTitle(
393 PhabricatorAuthProviderConfigTransaction
$xaction) {
398 public function readFormValuesFromProvider() {
402 public function readFormValuesFromRequest(AphrontRequest
$request) {
406 public function processEditForm(
407 AphrontRequest
$request,
413 return array($errors, $issues, $values);
416 public function extendEditForm(
417 AphrontRequest
$request,
418 AphrontFormView
$form,
425 public function willRenderLinkedAccount(
426 PhabricatorUser
$viewer,
427 PHUIObjectItemView
$item,
428 PhabricatorExternalAccount
$account) {
430 $account_view = id(new PhabricatorAuthAccountView())
431 ->setExternalAccount($account)
432 ->setAuthProvider($this);
438 'class' => 'mmr mml mst mmb',
444 * Return true to use a two-step configuration (setup, configure) instead of
445 * the default single-step configuration. In practice, this means that
446 * creating a new provider instance will redirect back to the edit page
447 * instead of the provider list.
449 * @return bool True if this provider uses two-step configuration.
451 public function hasSetupStep() {
456 * Render a standard login/register button element.
458 * The `$attributes` parameter takes these keys:
460 * - `uri`: URI the button should take the user to when clicked.
461 * - `method`: Optional HTTP method the button should use, defaults to GET.
463 * @param AphrontRequest HTTP request.
464 * @param string Request mode string.
465 * @param map Additional parameters, see above.
466 * @return wild Log in button.
468 protected function renderStandardLoginButton(
469 AphrontRequest
$request,
471 array $attributes = array()) {
473 PhutilTypeSpec
::checkMap(
476 'method' => 'optional string',
478 'sigil' => 'optional string',
481 $viewer = $request->getUser();
482 $adapter = $this->getAdapter();
484 if ($mode == 'link') {
485 $button_text = pht('Link External Account');
486 } else if ($mode == 'refresh') {
487 $button_text = pht('Refresh Account Link');
488 } else if ($mode == 'invite') {
489 $button_text = pht('Register Account');
490 } else if ($this->shouldAllowRegistration()) {
491 $button_text = pht('Log In or Register');
493 $button_text = pht('Log In');
496 $icon = id(new PHUIIconView())
497 ->setSpriteSheet(PHUIIconView
::SPRITE_LOGIN
)
498 ->setSpriteIcon($this->getLoginIcon());
500 $button = id(new PHUIButtonView())
501 ->setSize(PHUIButtonView
::BIG
)
502 ->setColor(PHUIButtonView
::GREY
)
504 ->setText($button_text)
505 ->setSubtext($this->getProviderName());
507 $uri = $attributes['uri'];
508 $uri = new PhutilURI($uri);
509 $params = $uri->getQueryParamsAsPairList();
510 $uri->removeAllQueryParams();
512 $content = array($button);
514 foreach ($params as $pair) {
515 list($key, $value) = $pair;
516 $content[] = phutil_tag(
525 $static_response = CelerityAPI
::getStaticResourceResponse();
526 $static_response->addContentSecurityPolicyURI('form-action', (string)$uri);
528 foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {
529 $static_response->addContentSecurityPolicyURI('form-action', $csp_uri);
532 return phabricator_form(
535 'method' => idx($attributes, 'method', 'GET'),
536 'action' => (string)$uri,
537 'sigil' => idx($attributes, 'sigil'),
542 public function renderConfigurationFooter() {
546 public function getAuthCSRFCode(AphrontRequest
$request) {
547 $phcid = $request->getCookie(PhabricatorCookies
::COOKIE_CLIENTID
);
548 if (!strlen($phcid)) {
549 throw new AphrontMalformedRequestException(
550 pht('Missing Client ID Cookie'),
552 'Your browser did not submit a "%s" cookie with client state '.
553 'information in the request. Check that cookies are enabled. '.
554 'If this problem persists, you may need to clear your cookies.',
555 PhabricatorCookies
::COOKIE_CLIENTID
),
559 return PhabricatorHash
::weakDigest($phcid);
562 protected function verifyAuthCSRFCode(AphrontRequest
$request, $actual) {
563 $expect = $this->getAuthCSRFCode($request);
565 if (!strlen($actual)) {
568 'The authentication provider did not return a client state '.
569 'parameter in its response, but one was expected. If this '.
570 'problem persists, you may need to clear your cookies.'));
573 if (!phutil_hashes_are_identical($actual, $expect)) {
576 'The authentication provider did not return the correct client '.
577 'state parameter in its response. If this problem persists, you may '.
578 'need to clear your cookies.'));
582 public function supportsAutoLogin() {
586 public function getAutoLoginURI(AphrontRequest
$request) {
587 throw new PhutilMethodNotImplementedException();
590 protected function getContentSecurityPolicyFormActions() {