diff --git a/conf/default.conf.php b/conf/default.conf.php index 5caf71509c..276e9df1b2 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -176,11 +176,25 @@ return array( 'amazon-ses.secret-key' => null, +// -- Auth ------------------------------------------------------------------ // + + // Can users login with a username/password, or by following the link from + // a password reset email? You can disable this and configure one or more + // OAuth providers instead. + 'auth.password-auth-enabled' => true, + // -- Facebook ------------------------------------------------------------ // // Can users use Facebook credentials to login to Phabricator? 'facebook.auth-enabled' => false, + // Can users use Facebook credentials to create new Phabricator accounts? + 'facebook.registration-enabled' => true, + + // Are Facebook accounts permanently linked to Phabricator accounts, or can + // the user unlink them? + 'facebook.auth-permanent' => false, + // The Facebook "Application ID" to use for Facebook API access. 'facebook.application-id' => null, @@ -193,6 +207,13 @@ return array( // Can users use Github credentials to login to Phabricator? 'github.auth-enabled' => false, + // Can users use Github credentials to create new Phabricator accounts? + 'github.registration-enabled' => true, + + // Are Github accounts permanently linked to Phabricator accounts, or can + // the user unlink them? + 'github.auth-permanent' => false, + // The Github "Client ID" to use for Github API access. 'github.application-id' => null, @@ -252,4 +273,7 @@ return array( 'aphront.default-application-configuration-class' => 'AphrontDefaultApplicationConfiguration', + + 'controller.oauth-registration' => + 'PhabricatorOAuthDefaultRegistrationController', ); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 86549d07f0..a5823a6c62 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -212,12 +212,14 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists', 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', + 'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default', 'PhabricatorOAuthDiagnosticsController' => 'applications/auth/controller/oauthdiagnostics', 'PhabricatorOAuthFailureView' => 'applications/auth/view/oauthfailure', 'PhabricatorOAuthLoginController' => 'applications/auth/controller/oauth', 'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/base', 'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/facebook', 'PhabricatorOAuthProviderGithub' => 'applications/auth/oauth/provider/github', + 'PhabricatorOAuthRegistrationController' => 'applications/auth/controller/oauthregistration/base', 'PhabricatorOAuthUnlinkController' => 'applications/auth/controller/unlink', 'PhabricatorObjectHandle' => 'applications/phid/handle', 'PhabricatorObjectHandleData' => 'applications/phid/handle/data', @@ -463,11 +465,13 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', + 'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController', 'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorOAuthFailureView' => 'AphrontView', 'PhabricatorOAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderGithub' => 'PhabricatorOAuthProvider', + 'PhabricatorOAuthRegistrationController' => 'PhabricatorAuthController', 'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorPHID' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController', diff --git a/src/aphront/console/core/DarkConsoleCore.php b/src/aphront/console/core/DarkConsoleCore.php index d87cec6642..afb9d09350 100755 --- a/src/aphront/console/core/DarkConsoleCore.php +++ b/src/aphront/console/core/DarkConsoleCore.php @@ -88,6 +88,7 @@ final class DarkConsoleCore { $visible = $user->getConsoleVisible(); if (!isset($plugins[$selected])) { + reset($plugins); $selected = key($plugins); } diff --git a/src/aphront/controller/AphrontController.php b/src/aphront/controller/AphrontController.php index 0ba389fd05..a70e905128 100644 --- a/src/aphront/controller/AphrontController.php +++ b/src/aphront/controller/AphrontController.php @@ -41,4 +41,8 @@ abstract class AphrontController { return $this->request; } + final public function delegateToController(AphrontController $controller) { + return $controller->processRequest(); + } + } diff --git a/src/applications/auth/controller/email/PhabricatorEmailLoginController.php b/src/applications/auth/controller/email/PhabricatorEmailLoginController.php index 23ea196795..bb43020bfc 100644 --- a/src/applications/auth/controller/email/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/email/PhabricatorEmailLoginController.php @@ -25,6 +25,10 @@ class PhabricatorEmailLoginController extends PhabricatorAuthController { public function processRequest() { $request = $this->getRequest(); + if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) { + return new Aphront400Response(); + } + $e_email = true; $e_captcha = true; $errors = array(); diff --git a/src/applications/auth/controller/email/__init__.php b/src/applications/auth/controller/email/__init__.php index 72d4c63071..d5e2f5174e 100644 --- a/src/applications/auth/controller/email/__init__.php +++ b/src/applications/auth/controller/email/__init__.php @@ -6,6 +6,7 @@ +phutil_require_module('phabricator', 'aphront/response/400'); phutil_require_module('phabricator', 'applications/auth/controller/base'); phutil_require_module('phabricator', 'applications/metamta/storage/mail'); phutil_require_module('phabricator', 'applications/people/storage/user'); diff --git a/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php b/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php index abfd26e89e..bc922aced2 100644 --- a/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php +++ b/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php @@ -31,6 +31,10 @@ class PhabricatorEmailTokenController extends PhabricatorAuthController { public function processRequest() { $request = $this->getRequest(); + if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) { + return new Aphront400Response(); + } + $token = $this->token; $email = $request->getStr('email'); diff --git a/src/applications/auth/controller/emailtoken/__init__.php b/src/applications/auth/controller/emailtoken/__init__.php index fd917cbcc1..cc20581f39 100644 --- a/src/applications/auth/controller/emailtoken/__init__.php +++ b/src/applications/auth/controller/emailtoken/__init__.php @@ -6,9 +6,11 @@ +phutil_require_module('phabricator', 'aphront/response/400'); phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/auth/controller/base'); phutil_require_module('phabricator', 'applications/people/storage/user'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/error'); diff --git a/src/applications/auth/controller/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php index e6991fae06..f55cc98662 100644 --- a/src/applications/auth/controller/login/PhabricatorLoginController.php +++ b/src/applications/auth/controller/login/PhabricatorLoginController.php @@ -30,69 +30,73 @@ class PhabricatorLoginController extends PhabricatorAuthController { return id(new AphrontRedirectResponse())->setURI('/'); } - $error = false; - $username = $request->getCookie('phusr'); - if ($request->isFormPost()) { - $username = $request->getStr('username'); + $password_auth = PhabricatorEnv::getEnvConfig('auth.password-auth-enabled'); - $user = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $username); - - $okay = false; - if ($user) { - if ($user->comparePassword($request->getStr('password'))) { - - $session_key = $user->establishSession('web'); - - $request->setCookie('phusr', $user->getUsername()); - $request->setCookie('phsid', $session_key); - - return id(new AphrontRedirectResponse()) - ->setURI('/'); - } - } - - if (!$okay) { - $request->clearCookie('phusr'); - $request->clearCookie('phsid'); - } - - $error = true; - } + $forms = array(); $error_view = null; - if ($error) { - $error_view = new AphrontErrorView(); - $error_view->setTitle('Bad username/password.'); + if ($password_auth) { + $error = false; + $username = $request->getCookie('phusr'); + if ($request->isFormPost()) { + $username = $request->getStr('username'); + + $user = id(new PhabricatorUser())->loadOneWhere( + 'username = %s', + $username); + + $okay = false; + if ($user) { + if ($user->comparePassword($request->getStr('password'))) { + + $session_key = $user->establishSession('web'); + + $request->setCookie('phusr', $user->getUsername()); + $request->setCookie('phsid', $session_key); + + return id(new AphrontRedirectResponse()) + ->setURI('/'); + } + } + + if (!$okay) { + $request->clearCookie('phusr'); + $request->clearCookie('phsid'); + } + + $error = true; + } + + if ($error) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Bad username/password.'); + } + + $form = new AphrontFormView(); + $form + ->setUser($request->getUser()) + ->setAction('/login/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Username/Email') + ->setName('username') + ->setValue($username)) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel('Password') + ->setName('password') + ->setCaption( + ''. + 'Forgot your password? / Email Login')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Login')); + + + // $panel->setCreateButton('Register New Account', '/login/register/'); + $forms['Phabricator Login'] = $form; } - $form = new AphrontFormView(); - $form - ->setUser($request->getUser()) - ->setAction('/login/') - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Username/Email') - ->setName('username') - ->setValue($username)) - ->appendChild( - id(new AphrontFormPasswordControl()) - ->setLabel('Password') - ->setName('password') - ->setCaption( - 'Forgot your password? / Email Login')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Login')); - - - $panel = new AphrontPanelView(); - $panel->setHeader('Phabricator Login'); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); -// $panel->setCreateButton('Register New Account', '/login/register/'); - $panel->appendChild($form); - $providers = array( PhabricatorOAuthProvider::PROVIDER_FACEBOOK, PhabricatorOAuthProvider::PROVIDER_GITHUB, @@ -117,6 +121,19 @@ class PhabricatorLoginController extends PhabricatorAuthController { // does not seem like the most severe threat in the world, and generating // CSRF for logged-out users is vaugely tricky. + if ($provider->isProviderRegistrationEnabled()) { + $title = "Login or Register with {$provider_name}"; + $body = "Login or register for Phabricator using your ". + "{$provider_name} account."; + $button = "Login or Register with {$provider_name}"; + } else { + $title = "Login with {$provider_name}"; + $body = "Login to your existing Phabricator account using your ". + "{$provider_name} account.

You can not use ". + "{$provider_name} to register a new account."; + $button = "Login with {$provider_name}"; + } + $auth_form = new AphrontFormView(); $auth_form ->setAction($auth_uri) @@ -126,16 +143,20 @@ class PhabricatorLoginController extends PhabricatorAuthController { ->setUser($request->getUser()) ->setMethod('GET') ->appendChild( - '

Login or register for '. - 'Phabricator using your '.$provider_name.' account.

') + '

'.$body.'

') ->appendChild( id(new AphrontFormSubmitControl()) - ->setValue("Login with {$provider_name} \xC2\xBB")); + ->setValue("{$button} \xC2\xBB")); - $panel->appendChild( - '

Login or Register with '.$provider_name.'

'); + $forms[$title] = $auth_form; + } - $panel->appendChild($auth_form); + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + foreach ($forms as $name => $form) { + $panel->appendChild('

'.$name.'

'); + $panel->appendChild($form); + $panel->appendChild('
'); } return $this->buildStandardPageResponse( diff --git a/src/applications/auth/controller/login/__init__.php b/src/applications/auth/controller/login/__init__.php index 532ff90b92..732f9d651c 100644 --- a/src/applications/auth/controller/login/__init__.php +++ b/src/applications/auth/controller/login/__init__.php @@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/auth/controller/base'); phutil_require_module('phabricator', 'applications/auth/oauth/provider/base'); phutil_require_module('phabricator', 'applications/people/storage/user'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/error'); diff --git a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php index 8386ca91ac..3dfcc2d99e 100644 --- a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php +++ b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php @@ -20,6 +20,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { private $provider; private $userID; + private $accessToken; private $tokenExpires; @@ -50,94 +51,31 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { return $this->buildErrorResponse($error_view); } - $token = $request->getStr('token'); - if (!$token) { - $client_id = $provider->getClientID(); - $client_secret = $provider->getClientSecret(); - $redirect_uri = $provider->getRedirectURI(); - $auth_uri = $provider->getTokenURI(); - - $code = $request->getStr('code'); - $query_data = array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'code' => $code, - ); - - $post_data = http_build_query($query_data); - $post_length = strlen($post_data); - - $stream_context = stream_context_create( - array( - 'http' => array( - 'method' => 'POST', - 'header' => - "Content-Type: application/x-www-form-urlencoded\r\n". - "Content-Length: {$post_length}\r\n", - 'content' => $post_data, - ), - )); - - $stream = fopen($auth_uri, 'r', false, $stream_context); - - $response = false; - $meta = null; - if ($stream) { - $meta = stream_get_meta_data($stream); - $response = stream_get_contents($stream); - fclose($stream); - } - - - if ($response === false) { - return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); - } - - $data = array(); - parse_str($response, $data); - - $token = idx($data, 'access_token'); - if (!$token) { - return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); - } - - if (idx($data, 'expires')) { - $this->tokenExpires = time() + $data['expires']; - } - - } else { - $this->tokenExpires = $request->getInt('expires'); + $error_response = $this->retrieveAccessToken($provider); + if ($error_response) { + return $error_response; } $userinfo_uri = new PhutilURI($provider->getUserInfoURI()); $userinfo_uri->setQueryParams( array( - 'access_token' => $token, + 'access_token' => $this->accessToken, )); $user_json = @file_get_contents($userinfo_uri); $user_data = json_decode($user_json, true); - $this->accessToken = $token; + $provider->setUserData($user_data); + $provider->setAccessToken($this->accessToken); - switch ($provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - $user_data = $user_data['user']; - break; - } - $this->userData = $user_data; + $user_id = $provider->retrieveUserID(); + $provider_key = $provider->getProviderKey(); - $user_id = $this->retrieveUserID(); - - $known_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere( - 'oauthProvider = %s and oauthUID = %s', - $provider->getProviderKey(), - $user_id); + $oauth_info = $this->retrieveOAuthInfo($provider); if ($current_user->getPHID()) { - if ($known_oauth) { - if ($known_oauth->getUserID() != $current_user->getID()) { + if ($oauth_info->getID()) { + if ($oauth_info->getUserID() != $current_user->getID()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Already Linked to Another Account'); @@ -156,6 +94,23 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { } } + $existing_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere( + 'userID = %d AND oauthProvider = %s', + $current_user->getID(), + $provider_key); + + if ($existing_oauth) { + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle('Already Linked to an Account From This Provider'); + $dialog->appendChild( + '

The account you are logged in with is already linked to a '. + $provider_name.' account. Before you can link it to a different '. + $provider_name.' account, you must unlink the old account.

'); + $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); @@ -163,17 +118,15 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { $dialog->appendChild( '

Link your '.$provider_name.' account to your Phabricator '. 'account?

'); - $dialog->addHiddenInput('token', $token); - $dialog->addHiddenInput('expires', $this->tokenExpires); + $dialog->addHiddenInput('token', $provider->getAccessToken()); + $dialog->addHiddenInput('expires', $oauth_info->getTokenExpires()); $dialog->addSubmitButton('Link Accounts'); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } - $oauth_info = new PhabricatorUserOAuthInfo(); $oauth_info->setUserID($current_user->getID()); - $this->configureOAuthInfo($oauth_info); $oauth_info->save(); return id(new AphrontRedirectResponse()) @@ -183,12 +136,11 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { // Login with known auth. - if ($known_oauth) { - $known_user = id(new PhabricatorUser())->load($known_oauth->getUserID()); + if ($oauth_info->getID()) { + $known_user = id(new PhabricatorUser())->load($oauth_info->getUserID()); $session_key = $known_user->establishSession('web'); - $this->configureOAuthInfo($known_oauth); - $known_oauth->save(); + $oauth_info->save(); $request->setCookie('phusr', $known_user->getUsername()); $request->setCookie('phsid', $session_key); @@ -196,10 +148,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { ->setURI('/'); } - // Merge accounts based on shared email. TODO: should probably get rid of - // this. - - $oauth_email = $this->retrieveUserEmail(); + $oauth_email = $provider->retrieveUserEmail(); if ($oauth_email) { $known_email = id(new PhabricatorUser()) ->loadOneWhere('email = %s', $oauth_email); @@ -218,159 +167,28 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { } } - $errors = array(); - $e_username = true; - $e_email = true; - $e_realname = true; + if (!$provider->isProviderRegistrationEnabled()) { + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle('No Account Registration With '.$provider_name); + $dialog->appendChild( + '

You can not register a new account using '.$provider_name.'; '. + 'you can only use your '.$provider_name.' account to log into an '. + 'existing Phabricator account which you have registered through '. + 'other means.

'); + $dialog->addCancelButton('/login/'); - $user = new PhabricatorUser(); - - $suggestion = $this->retrieveUsernameSuggestion(); - $user->setUsername($suggestion); - - $oauth_realname = $this->retreiveRealNameSuggestion(); - - if ($request->isFormPost()) { - - $user->setUsername($request->getStr('username')); - $username = $user->getUsername(); - $matches = null; - if (!strlen($user->getUsername())) { - $e_username = 'Required'; - $errors[] = 'Username is required.'; - } else if (!preg_match('/^[a-zA-Z0-9]+$/', $username, $matches)) { - $e_username = 'Invalid'; - $errors[] = 'Username may only contain letters and numbers.'; - } else { - $e_username = null; - } - - if ($oauth_email) { - $user->setEmail($oauth_email); - } else { - $user->setEmail($request->getStr('email')); - if (!strlen($user->getEmail())) { - $e_email = 'Required'; - $errors[] = 'Email is required.'; - } else { - $e_email = null; - } - } - - if ($oauth_realname) { - $user->setRealName($oauth_realname); - } else { - $user->setRealName($request->getStr('realname')); - if (!strlen($user->getStr('realname'))) { - $e_realname = 'Required'; - $errors[] = 'Real name is required.'; - } else { - $e_realname = null; - } - } - - if (!$errors) { - $image = $this->retreiveProfileImageSuggestion(); - if ($image) { - $file = PhabricatorFile::newFromFileData( - $image, - array( - 'name' => $provider->getProviderKey().'-profile.jpg' - )); - $user->setProfileImagePHID($file->getPHID()); - } - - try { - $user->save(); - - $oauth_info = new PhabricatorUserOAuthInfo(); - $oauth_info->setUserID($user->getID()); - $this->configureOAuthInfo($oauth_info); - $oauth_info->save(); - - $session_key = $user->establishSession('web'); - $request->setCookie('phusr', $user->getUsername()); - $request->setCookie('phsid', $session_key); - return id(new AphrontRedirectResponse())->setURI('/'); - } catch (AphrontQueryDuplicateKeyException $exception) { - - $same_username = id(new PhabricatorUser())->loadOneWhere( - 'userName = %s', - $user->getUserName()); - - $same_email = id(new PhabricatorUser())->loadOneWhere( - 'email = %s', - $user->getEmail()); - - if ($same_username) { - $e_username = 'Duplicate'; - $errors[] = 'That username or email is not unique.'; - } else if ($same_email) { - $e_email = 'Duplicate'; - $errors[] = 'That email is not unique.'; - } else { - throw $exception; - } - } - } + return id(new AphrontDialogResponse())->setDialog($dialog); } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle('Registration Failed'); - $error_view->setErrors($errors); - } + $class = PhabricatorEnv::getEnvConfig('controller.oauth-registration'); + PhutilSymbolLoader::loadClass($class); + $controller = newv($class, array($this->getRequest())); - $form = new AphrontFormView(); - $form - ->addHiddenInput('token', $token) - ->addHiddenInput('expires', $this->tokenExpires) - ->setUser($request->getUser()) - ->setAction($provider->getRedirectURI()) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Username') - ->setName('username') - ->setValue($user->getUsername()) - ->setError($e_username)); + $controller->setOAuthProvider($provider); + $controller->setOAuthInfo($oauth_info); - if (!$oauth_email) { - $form->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Email') - ->setName('email') - ->setValue($request->getStr('email')) - ->setError($e_email)); - } - - if (!$oauth_realname) { - $form->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Real Name') - ->setName('realname') - ->setValue($request->getStr('realname')) - ->setError($e_realname)); - } - - $form - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Create Account')); - - $panel = new AphrontPanelView(); - $panel->setHeader('Create New Account'); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); - $panel->appendChild($form); - - return $this->buildStandardPageResponse( - array( - $error_view, - $panel, - ), - array( - 'title' => 'Create New Account', - )); + return $this->delegateToController($controller); } private function buildErrorResponse(PhabricatorOAuthFailureView $view) { @@ -386,71 +204,90 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { )); } - private function retrieveUserID() { - return $this->userData['id']; - } + private function retrieveAccessToken(PhabricatorOAuthProvider $provider) { + $request = $this->getRequest(); - private function retrieveUserEmail() { - return $this->userData['email']; - } - - private function retrieveUsernameSuggestion() { - switch ($this->provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: - $matches = null; - $link = $this->userData['link']; - if (preg_match('@/([a-zA-Z0-9]+)$@', $link, $matches)) { - return $matches[1]; - } - break; - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - return $this->userData['login']; + $token = $request->getStr('token'); + if ($token) { + $this->tokenExpires = $request->getInt('expires'); + $this->accessToken = $token; + return null; } + + $client_id = $provider->getClientID(); + $client_secret = $provider->getClientSecret(); + $redirect_uri = $provider->getRedirectURI(); + $auth_uri = $provider->getTokenURI(); + + $code = $request->getStr('code'); + $query_data = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'code' => $code, + ); + + $post_data = http_build_query($query_data); + $post_length = strlen($post_data); + + $stream_context = stream_context_create( + array( + 'http' => array( + 'method' => 'POST', + 'header' => + "Content-Type: application/x-www-form-urlencoded\r\n". + "Content-Length: {$post_length}\r\n", + 'content' => $post_data, + ), + )); + + $stream = fopen($auth_uri, 'r', false, $stream_context); + + $response = false; + $meta = null; + if ($stream) { + $meta = stream_get_meta_data($stream); + $response = stream_get_contents($stream); + fclose($stream); + } + + if ($response === false) { + return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); + } + + $data = array(); + parse_str($response, $data); + + $token = idx($data, 'access_token'); + if (!$token) { + return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); + } + + if (idx($data, 'expires')) { + $this->tokenExpires = time() + $data['expires']; + } + + $this->accessToken = $token; + return null; } - private function retreiveProfileImageSuggestion() { - switch ($this->provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: - $uri = 'https://graph.facebook.com/me/picture?access_token='; - return @file_get_contents($uri.$this->accessToken); - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - $id = $this->userData['gravatar_id']; - if ($id) { - $uri = 'http://www.gravatar.com/avatar/'.$id.'?s=50'; - return @file_get_contents($uri); - } + private function retrieveOAuthInfo(PhabricatorOAuthProvider $provider) { + + $oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere( + 'oauthProvider = %s and oauthUID = %s', + $provider->getProviderKey(), + $provider->retrieveUserID()); + + if (!$oauth_info) { + $oauth_info = new PhabricatorUserOAuthInfo(); + $oauth_info->setOAuthProvider($provider->getProviderKey()); + $oauth_info->setOAuthUID($provider->retrieveUserID()); } - return null; - } - private function retrieveAccountURI() { - switch ($this->provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: - return $this->userData['link']; - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - $username = $this->retrieveUsernameSuggestion(); - if ($username) { - return 'https://github.com/'.$username; - } - return null; - } - return null; - } - - private function retreiveRealNameSuggestion() { - return $this->userData['name']; - } - - private function configureOAuthInfo(PhabricatorUserOAuthInfo $oauth_info) { - $provider = $this->provider; - - $oauth_info->setOAuthProvider($provider->getProviderKey()); - $oauth_info->setOAuthUID($this->retrieveUserID()); - $oauth_info->setAccountURI($this->retrieveAccountURI()); - $oauth_info->setAccountName($this->retrieveUserNameSuggestion()); - - $oauth_info->setToken($this->accessToken); + $oauth_info->setAccountURI($provider->retrieveUserAccountURI()); + $oauth_info->setAccountName($provider->retrieveUserAccountName()); + $oauth_info->setToken($provider->getAccessToken()); $oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD); // If we have out-of-date expiration info, just clear it out. Then replace @@ -463,6 +300,8 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { $expires = $this->tokenExpires; } $oauth_info->setTokenExpires($expires); + + return $oauth_info; } } diff --git a/src/applications/auth/controller/oauth/__init__.php b/src/applications/auth/controller/oauth/__init__.php index 19082b7aa2..b3ee8dda26 100644 --- a/src/applications/auth/controller/oauth/__init__.php +++ b/src/applications/auth/controller/oauth/__init__.php @@ -12,17 +12,13 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/auth/controller/base'); phutil_require_module('phabricator', 'applications/auth/oauth/provider/base'); phutil_require_module('phabricator', 'applications/auth/view/oauthfailure'); -phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'view/dialog'); -phutil_require_module('phabricator', 'view/form/base'); -phutil_require_module('phabricator', 'view/form/control/submit'); -phutil_require_module('phabricator', 'view/form/control/text'); -phutil_require_module('phabricator', 'view/form/error'); -phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phutil', 'parser/uri'); +phutil_require_module('phutil', 'symbols'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/auth/controller/oauthregistration/base/PhabricatorOAuthRegistrationController.php b/src/applications/auth/controller/oauthregistration/base/PhabricatorOAuthRegistrationController.php new file mode 100644 index 0000000000..addbe914ee --- /dev/null +++ b/src/applications/auth/controller/oauthregistration/base/PhabricatorOAuthRegistrationController.php @@ -0,0 +1,43 @@ +oauthInfo = $info; + return $this; + } + + final public function getOAuthInfo() { + return $this->oauthInfo; + } + + final public function setOAuthProvider($provider) { + $this->oauthProvider = $provider; + return $this; + } + + final public function getOAuthProvider() { + return $this->oauthProvider; + } + +} diff --git a/src/applications/auth/controller/oauthregistration/base/__init__.php b/src/applications/auth/controller/oauthregistration/base/__init__.php new file mode 100644 index 0000000000..9a240054a7 --- /dev/null +++ b/src/applications/auth/controller/oauthregistration/base/__init__.php @@ -0,0 +1,12 @@ +getOAuthProvider(); + $oauth_info = $this->getOAuthInfo(); + $request = $this->getRequest(); + + $errors = array(); + $e_username = true; + $e_email = true; + $e_realname = true; + + $user = new PhabricatorUser(); + + $user->setUsername($provider->retrieveUserAccountName()); + $user->setRealName($provider->retrieveUserRealName()); + $user->setEmail($provider->retrieveUserEmail()); + + if ($request->isFormPost()) { + + $user->setUsername($request->getStr('username')); + $username = $user->getUsername(); + $matches = null; + if (!strlen($user->getUsername())) { + $e_username = 'Required'; + $errors[] = 'Username is required.'; + } else if (!preg_match('/^[a-zA-Z0-9]+$/', $username, $matches)) { + $e_username = 'Invalid'; + $errors[] = 'Username may only contain letters and numbers.'; + } else { + $e_username = null; + } + + if ($user->getEmail() === null) { + $user->setEmail($request->getStr('email')); + if (!strlen($user->getEmail())) { + $e_email = 'Required'; + $errors[] = 'Email is required.'; + } else { + $e_email = null; + } + } + + if ($user->getRealName() === null) { + $user->setRealName($request->getStr('realname')); + if (!strlen($user->getStr('realname'))) { + $e_realname = 'Required'; + $errors[] = 'Real name is required.'; + } else { + $e_realname = null; + } + } + + if (!$errors) { + $image = $provider->retreiveUserProfileImage(); + if ($image) { + $file = PhabricatorFile::newFromFileData( + $image, + array( + 'name' => $provider->getProviderKey().'-profile.jpg' + )); + $user->setProfileImagePHID($file->getPHID()); + } + + try { + $user->save(); + + $oauth_info->setUserID($user->getID()); + $oauth_info->save(); + + $session_key = $user->establishSession('web'); + $request->setCookie('phusr', $user->getUsername()); + $request->setCookie('phsid', $session_key); + return id(new AphrontRedirectResponse())->setURI('/'); + } catch (AphrontQueryDuplicateKeyException $exception) { + + $same_username = id(new PhabricatorUser())->loadOneWhere( + 'userName = %s', + $user->getUserName()); + + $same_email = id(new PhabricatorUser())->loadOneWhere( + 'email = %s', + $user->getEmail()); + + if ($same_username) { + $e_username = 'Duplicate'; + $errors[] = 'That username or email is not unique.'; + } else if ($same_email) { + $e_email = 'Duplicate'; + $errors[] = 'That email is not unique.'; + } else { + throw $exception; + } + } + } + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Registration Failed'); + $error_view->setErrors($errors); + } + + $form = new AphrontFormView(); + $form + ->addHiddenInput('token', $provider->getAccessToken()) + ->addHiddenInput('expires', $oauth_info->getTokenExpires()) + ->setUser($request->getUser()) + ->setAction($provider->getRedirectURI()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Username') + ->setName('username') + ->setValue($user->getUsername()) + ->setError($e_username)); + + if ($provider->retrieveUserEmail() === null) { + $form->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Email') + ->setName('email') + ->setValue($request->getStr('email')) + ->setError($e_email)); + } + + if ($provider->retrieveUserRealName () === null) { + $form->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Real Name') + ->setName('realname') + ->setValue($request->getStr('realname')) + ->setError($e_realname)); + } + + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Create Account')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Create New Account'); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create New Account', + )); + } + +} diff --git a/src/applications/auth/controller/oauthregistration/default/__init__.php b/src/applications/auth/controller/oauthregistration/default/__init__.php new file mode 100644 index 0000000000..ba332f4cf1 --- /dev/null +++ b/src/applications/auth/controller/oauthregistration/default/__init__.php @@ -0,0 +1,22 @@ +getUser(); $provider = $this->provider; + + if ($provider->isProviderLinkPermanent()) { + throw new Exception( + "You may not unlink accounts from this OAuth provider."); + } + $provider_name = $provider->getProviderName(); $provider_key = $provider->getProviderKey(); diff --git a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php index dd3590a9c6..f5124eaf81 100644 --- a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php +++ b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php @@ -21,9 +21,13 @@ abstract class PhabricatorOAuthProvider { const PROVIDER_FACEBOOK = 'facebook'; const PROVIDER_GITHUB = 'github'; + private $accessToken; + abstract public function getProviderKey(); abstract public function getProviderName(); abstract public function isProviderEnabled(); + abstract public function isProviderLinkPermanent(); + abstract public function isProviderRegistrationEnabled(); abstract public function getRedirectURI(); abstract public function getClientID(); abstract public function getClientSecret(); @@ -32,10 +36,27 @@ abstract class PhabricatorOAuthProvider { abstract public function getUserInfoURI(); abstract public function getMinimumScope(); + abstract public function setUserData($data); + abstract public function retrieveUserID(); + abstract public function retrieveUserEmail(); + abstract public function retrieveUserAccountName(); + abstract public function retrieveUserProfileImage(); + abstract public function retrieveUserAccountURI(); + abstract public function retrieveUserRealName(); + public function __construct() { } + final public function setAccessToken($access_token) { + $this->accessToken = $access_token; + return $this; + } + + final public function getAccessToken() { + return $this->accessToken; + } + public static function newProvider($which) { switch ($which) { case self::PROVIDER_FACEBOOK: diff --git a/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php b/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php index 8893f9a178..ec97334a1f 100644 --- a/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php +++ b/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php @@ -18,6 +18,8 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { + private $userData; + public function getProviderKey() { return self::PROVIDER_FACEBOOK; } @@ -30,6 +32,14 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { return PhabricatorEnv::getEnvConfig('facebook.auth-enabled'); } + public function isProviderLinkPermanent() { + return PhabricatorEnv::getEnvConfig('facebook.auth-permanent'); + } + + public function isProviderRegistrationEnabled() { + return PhabricatorEnv::getEnvConfig('facebook.registration-enabled'); + } + public function getRedirectURI() { return PhabricatorEnv::getURI('/oauth/facebook/login/'); } @@ -58,4 +68,39 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { return 'email'; } + public function setUserData($data) { + $this->userData = $data; + return $this; + } + + public function retrieveUserID() { + return $this->userData['id']; + } + + public function retrieveUserEmail() { + return $this->userData['email']; + } + + public function retrieveUserAccountName() { + $matches = null; + $link = $this->userData['link']; + if (preg_match('@/([a-zA-Z0-9]+)$@', $link, $matches)) { + return $matches[1]; + } + return null; + } + + public function retrieveUserProfileImage() { + $uri = 'https://graph.facebook.com/me/picture?access_token='; + return @file_get_contents($uri.$this->getAccessToken()); + } + + public function retrieveUserAccountURI() { + return $this->userData['link']; + } + + public function retrieveUserRealName() { + return $this->userData['name']; + } + } diff --git a/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php b/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php index 7b92956a21..f5834369ae 100644 --- a/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php +++ b/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php @@ -18,6 +18,8 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { + private $userData; + public function getProviderKey() { return self::PROVIDER_GITHUB; } @@ -30,6 +32,14 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { return PhabricatorEnv::getEnvConfig('github.auth-enabled'); } + public function isProviderLinkPermanent() { + return PhabricatorEnv::getEnvConfig('github.auth-permanent'); + } + + public function isProviderRegistrationEnabled() { + return PhabricatorEnv::getEnvConfig('github.registration-enabled'); + } + public function getRedirectURI() { return PhabricatorEnv::getURI('/oauth/github/login/'); } @@ -58,4 +68,42 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { return null; } + public function setUserData($data) { + $this->userData = $data['user']; + return $this; + } + + public function retrieveUserID() { + return $this->userData['id']; + } + + public function retrieveUserEmail() { + return $this->userData['email']; + } + + public function retrieveUserAccountName() { + return $this->userData['login']; + } + + public function retrieveUserProfileImage() { + $id = $this->userData['gravatar_id']; + if ($id) { + $uri = 'http://www.gravatar.com/avatar/'.$id.'?s=50'; + return @file_get_contents($uri); + } + return null; + } + + public function retrieveUserAccountURI() { + $username = $this->retrieveUserAccountName(); + if ($username) { + return 'https://github.com/'.$username; + } + return null; + } + + public function retrieveUserRealName() { + return $this->userData['name']; + } + } diff --git a/src/applications/people/controller/settings/PhabricatorUserSettingsController.php b/src/applications/people/controller/settings/PhabricatorUserSettingsController.php index 8202d8a226..fea4fa4092 100644 --- a/src/applications/people/controller/settings/PhabricatorUserSettingsController.php +++ b/src/applications/people/controller/settings/PhabricatorUserSettingsController.php @@ -358,18 +358,20 @@ class PhabricatorUserSettingsController extends PhabricatorPeopleController { ->setLabel($provider_name.' URI') ->setValue($oauth_info->getAccountURI())); - $unlink = 'Unlink '.$provider_name.' Account'; - $unlink_form = new AphrontFormView(); - $unlink_form - ->setUser($user) - ->appendChild( - '

You may unlink this account '. - 'from your '.$provider_name.' account. This will prevent you from '. - 'logging in with your '.$provider_name.' credentials.

') - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton('/oauth/'.$provider_key.'/unlink/', $unlink)); - $forms['Unlink Account'] = $unlink_form; + if (!$provider->isProviderLinkPermanent()) { + $unlink = 'Unlink '.$provider_name.' Account'; + $unlink_form = new AphrontFormView(); + $unlink_form + ->setUser($user) + ->appendChild( + '

You may unlink this account '. + 'from your '.$provider_name.' account. This will prevent you from '. + 'logging in with your '.$provider_name.' credentials.

') + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/oauth/'.$provider_key.'/unlink/', $unlink)); + $forms['Unlink Account'] = $unlink_form; + } $expires = $oauth_info->getTokenExpires(); if ($expires) {