1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-09-20 09:18:48 +02:00

Provide start screen and full registration flow on the new auth stuff

Summary:
Ref T1536. Code is intentionally made unreachable (see PhabricatorAuthProviderOAuthFacebook->isEnabled()).

This adds:

  - A provider-driven "start" screen (this has the list of ways you can login/register).
  - Registration actually works.
  - Facebook OAuth works.

@chad, do you have any design ideas on the start screen? I think we poked at it before, but the big issue was that there were a limitless number of providers. Today, we have:

  - Password
  - LDAP
  - Facebook
  - GitHub
  - Phabricator
  - Disqus
  - Google

We plan to add:

  - Asana
  - An arbitrary number of additional instances of Phabricator

Users want to add:

  - OpenID
  - Custom providers

And I'd like to have these at some point:

  - Stripe
  - WePay
  - Amazon
  - Bitbucket

So basically any UI for this has to accommodate 300 zillion auth options. I don't think we need to solve any UX problems here (realistically, installs enable 1-2 auth options and users don't actually face an overwhelming number of choices) but making the login forms less ugly would be nice. No combination of prebuilt elements seems to look very good for this use case.

Test Plan: Registered a new acount with Facebook.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1536

Differential Revision: https://secure.phabricator.com/D6161
This commit is contained in:
epriestley 2013-06-16 10:15:16 -07:00
parent 7efee51c38
commit c108ada7e4
7 changed files with 327 additions and 10 deletions

View file

@ -817,7 +817,10 @@ phutil_register_library_map(array(
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
'PhabricatorAuthProviderOAuth' => 'applications/auth/provider/PhabricatorAuthProviderOAuth.php',
'PhabricatorAuthProviderOAuthFacebook' => 'applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php',
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php',
'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
@ -2672,7 +2675,10 @@ phutil_register_library_map(array(
'PhabricatorAuditReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthProviderOAuth' => 'PhabricatorAuthProvider',
'PhabricatorAuthProviderOAuthFacebook' => 'PhabricatorAuthProviderOAuth',
'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorBarePageExample' => 'PhabricatorUIExample',
'PhabricatorBarePageView' => 'AphrontPageView',

View file

@ -10,6 +10,10 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication {
return false;
}
public function getBaseURI() {
return '/auth/';
}
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
@ -34,6 +38,7 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication {
'/auth/' => array(
'login/(?P<pkey>[^/]+)/' => 'PhabricatorAuthLoginController',
'register/(?P<akey>[^/]+)/' => 'PhabricatorAuthRegisterController',
'start/' => 'PhabricatorAuthStartController',
),
);
}

View file

@ -0,0 +1,157 @@
<?php
final class PhabricatorAuthStartController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
// Kick the user home if they are already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
return $this->processAjaxRequest();
}
if ($request->isConduit()) {
return $this->processConduitRequest();
}
if ($request->getCookie('phusr') && $request->getCookie('phsid')) {
// The session cookie is invalid, so clear it.
$request->clearCookie('phusr');
$request->clearCookie('phsid');
return $this->renderError(
pht(
"Your login session is invalid. Try reloading the page and logging ".
"in again. If that does not work, clear your browser cookies."));
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowLogin()) {
unset($providers[$key]);
}
}
if (!$providers) {
return $this->renderError(
pht(
"This Phabricator install is not configured with any enabled ".
"authentication providers which can be used to log in."));
}
$next_uri = $request->getStr('next');
if (!$next_uri) {
$next_uri_path = $this->getRequest()->getPath();
if ($next_uri_path == '/auth/start/') {
$next_uri = '/';
} else {
$next_uri = $this->getRequest()->getRequestURI();
}
}
if (!$request->isFormPost()) {
$request->setCookie('next_uri', $next_uri);
}
$out = array();
foreach ($providers as $provider) {
$out[] = $provider->buildLoginForm($this);
}
$login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
$login_message = phutil_safe_html($login_message);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Login')));
return $this->buildApplicationPage(
array(
$crumbs,
$login_message,
$out,
),
array(
'title' => pht('Login to Phabricator'),
'device' => true,
'dust' => true,
));
}
private function processAjaxRequest() {
$request = $this->getRequest();
$viewer = $request->getViewer();
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login...".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
$dialog = new AphrontDialogView();
$dialog->setUser($viewer);
$dialog->setTitle(pht('Login Required'));
$dialog->appendChild(pht('You must login to continue.'));
$dialog->addSubmitButton(pht('Login'));
$dialog->addCancelButton('/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function processConduitRequest() {
$request = $this->getRequest();
$viewer = $request->getViewer();
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message = pht(
'ERROR: You are making a Conduit API request to "%s", but the correct '.
'HTTP request path to use in order to access a COnduit method is "%s" '.
'(for example, "%s"). Check your configuration.',
$request_path,
$conduit_path,
$example_path);
return id(new AphrontPlainTextResponse())->setContent($message);
}
private function renderError($message) {
$title = pht('Authentication Failure');
$view = new AphrontErrorView();
$view->setTitle($title);
$view->appendChild($message);
return $this->buildApplicationPage(
$view,
array(
'title' => $title,
'device' => true,
'dust' => true,
));
}
}

View file

@ -38,7 +38,7 @@ abstract class PhabricatorAuthProvider {
return $providers;
}
public static function getEnabledProviders() {
public static function getAllEnabledProviders() {
$providers = self::getAllProviders();
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
@ -49,15 +49,20 @@ abstract class PhabricatorAuthProvider {
}
public static function getEnabledProviderByKey($provider_key) {
return idx(self::getEnabledProviders(), $provider_key);
return idx(self::getAllEnabledProviders(), $provider_key);
}
abstract public function getProviderName();
abstract public function getAdapater();
abstract public function getAdapter();
abstract public function isEnabled();
abstract public function shouldAllowLogin();
abstract public function shouldAllowRegistration();
abstract public function shouldAllowAccountLink();
abstract public function shouldAllowAccountUnlink();
abstract public function buildLoginForm(
PhabricatorAuthStartController $controller);
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
@ -71,22 +76,38 @@ abstract class PhabricatorAuthProvider {
protected function loadOrCreateAccount($account_id) {
if (!strlen($account_id)) {
throw new Exception("loadOrCreateAccount(...): empty account ID!");
throw new Exception(
"loadOrCreateAccount(...): empty account ID!");
}
$adapter = $this->getAdapter();
$adapter_class = get_class($adapter);
if (!strlen($adapter->getAdapterType())) {
throw new Exception(
"AuthAdapter (of class '{$adapter_class}') has an invalid ".
"implementation: no adapter type.");
}
if (!strlen($adapter->getAdapterDomain())) {
throw new Exception(
"AuthAdapter (of class '{$adapter_class}') has an invalid ".
"implementation: no adapter domain.");
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s',
$adapter->getProviderType(),
$adapter->getProviderDomain(),
$adapter->getAdapterType(),
$adapter->getAdapterDomain(),
$account_id);
if (!$account) {
$account = id(new PhabricatorExternalAccount())
->setAccountType($adapter->getProviderType())
->setAccountDomain($adapter->getProviderDomain())
->setAccountType($adapter->getAdapterType())
->setAccountDomain($adapter->getAdapterDomain())
->setAccountID($account_id);
}
$account->setDisplayName('');
$account->setUsername($adapter->getAccountName());
$account->setRealName($adapter->getAccountRealName());
$account->setEmail($adapter->getAccountEmail());
@ -120,6 +141,10 @@ abstract class PhabricatorAuthProvider {
return $account;
}
protected function getLoginURI() {
$app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth');
$uri = $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
return PhabricatorEnv::getURI($uri);
}
}

View file

@ -2,6 +2,79 @@
abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider {
protected $adapter;
abstract protected function getOAuthClientID();
abstract protected function getOAuthClientSecret();
abstract protected function newOAuthAdapter();
public function getAdapter() {
if (!$this->adapter) {
$adapter = $this->newOAuthAdapter();
$this->adapter = $adapter;
$this->configureAdapter($adapter);
}
return $this->adapter;
}
public function isEnabled() {
return $this->getOAuthClientID() && $this->getOAuthClientSecret();
}
protected function configureAdapter(PhutilAuthAdapterOAuth $adapter) {
$adapter->setClientID($this->getOAuthClientID());
$adapter->setClientSecret($this->getOAuthClientSecret());
$adapter->setRedirectURI($this->getLoginURI());
return $adapter;
}
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
$viewer = $request->getUser();
$form = id(new AphrontFormView())
->setUser($viewer);
$submit = new AphrontFormSubmitControl();
if ($this->shouldAllowRegistration()) {
$submit->setValue(
pht("Login or Register with %s \xC2\xBB", $this->getProviderName()));
$header = pht("Login or Register with %s", $this->getProviderName());
} else {
$submit->setValue(
pht("Login with %s \xC2\xBB", $this->getProviderName()));
$header = pht("Login with %s", $this->getProviderName());
}
$form->appendChild($submit);
// TODO: This is pretty hideous.
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->setNoBackground(true);
$panel->appendChild($form);
$adapter = $this->getAdapter();
$uri = new PhutilURI($adapter->getAuthenticateURI());
$params = $uri->getQueryParams();
$uri->setQueryParams(array());
$form->setAction((string)$uri);
foreach ($params as $key => $value) {
$form->addHiddenInput($key, $value);
}
$form->setMethod('GET');
return $panel;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {

View file

@ -0,0 +1,51 @@
<?php
final class PhabricatorAuthProviderOAuthFacebook
extends PhabricatorAuthProviderOAuth {
public function getProviderName() {
return pht('Facebook');
}
protected function newOAuthAdapter() {
return new PhutilAuthAdapterOAuthFacebook();
}
public function isEnabled() {
// TODO: Remove this once we switch to the new auth mechanism.
return false &&
parent::isEnabled() &&
PhabricatorEnv::getEnvConfig('facebook.auth-enabled');
}
protected function getOAuthClientID() {
return PhabricatorEnv::getEnvConfig('facebook.application-id');
}
protected function getOAuthClientSecret() {
$secret = PhabricatorEnv::getEnvConfig('facebook.application-secret');
if ($secret) {
return new PhutilOpaqueEnvelope($secret);
}
return null;
}
public function shouldAllowLogin() {
return true;
}
public function shouldAllowRegistration() {
return PhabricatorEnv::getEnvConfig('facebook.registration-enabled');
}
public function shouldAllowAccountLink() {
return true;
}
public function shouldAllowAccountUnlink() {
return !PhabricatorEnv::getEnvConfig('facebook.auth-permanent');
}
}

View file

@ -38,7 +38,7 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO {
}
public function getProviderKey() {
return $this->getAccountType().':'.$this->accountDomain();
return $this->getAccountType().':'.$this->getAccountDomain();
}
public function save() {