diff --git a/conf/default.conf.php b/conf/default.conf.php
index 077787e9e2..3261b22c6a 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -98,6 +98,7 @@ return array(
'recaptcha.private-key',
'phabricator.csrf-key',
'facebook.application-secret',
+ 'github.secret',
),
// -- MySQL --------------------------------------------------------------- //
@@ -187,6 +188,27 @@ return array(
'facebook.application-secret' => null,
+// -- Github ---------------------------------------------------------------- //
+
+ // Can users use Github credentials to login to Phabricator?
+ 'github.auth-enabled' => false,
+
+ // The Github "Client ID" to use for Github API access.
+ 'github.application-id' => null,
+
+ // The Github "Secret" to use for Github API access.
+ 'github.application-secret' => null,
+
+
+ // Github Authorize URI. You don't need to change this unless Github changes
+ // its API in the future (this is unlikely).
+ 'github.authorize-uri' => 'https://github.com/login/oauth/authorize',
+
+ // Github Access Token URI. You don't need to change this unless Github
+ // changes its API in the future (this is unlikely).
+ 'github.access-token-uri' => 'https://github.com/login/oauth/access_token',
+
+
// -- Recaptcha ------------------------------------------------------------- //
// Is Recaptcha enabled? If disabled, captchas will not appear.
diff --git a/resources/sql/patches/002.oauth.sql b/resources/sql/patches/002.oauth.sql
new file mode 100644
index 0000000000..7cdaeb0a73
--- /dev/null
+++ b/resources/sql/patches/002.oauth.sql
@@ -0,0 +1,18 @@
+create table phabricator_user.user_oauthinfo (
+ id int unsigned not null auto_increment primary key,
+ userID int unsigned not null,
+ oauthProvider varchar(255) not null,
+ oauthUID varchar(255) not null,
+ unique key (userID, oauthProvider),
+ unique key (oauthProvider, oauthUID),
+ dateCreated int unsigned not null,
+ dateModified int unsigned not null
+);
+
+insert into phabricator_user.user_oauthinfo
+ (userID, oauthProvider, oauthUID, dateCreated, dateModified)
+ SELECT id, 'facebook', facebookUID, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+ FROM phabricator_user.user
+ WHERE facebookUID is not null;
+
+alter table phabricator_user.user drop facebookUID;
\ No newline at end of file
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 1e60390467..61286449f3 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -187,8 +187,6 @@ phutil_register_library_map(array(
'PhabricatorEmailLoginController' => 'applications/auth/controller/email',
'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken',
'PhabricatorEnv' => 'infrastructure/env',
- 'PhabricatorFacebookAuthController' => 'applications/auth/controller/facebookauth',
- 'PhabricatorFacebookAuthDiagnosticsController' => 'applications/auth/controller/facebookauth/diagnostics',
'PhabricatorFile' => 'applications/files/storage/file',
'PhabricatorFileController' => 'applications/files/controller/base',
'PhabricatorFileDAO' => 'applications/files/storage/base',
@@ -214,6 +212,12 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists',
'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
+ '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',
'PhabricatorObjectHandle' => 'applications/phid/handle',
'PhabricatorObjectHandleData' => 'applications/phid/handle/data',
'PhabricatorObjectSelectorDialog' => 'view/control/objectselector',
@@ -270,6 +274,7 @@ phutil_register_library_map(array(
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
'PhabricatorUser' => 'applications/people/storage/user',
'PhabricatorUserDAO' => 'applications/people/storage/base',
+ 'PhabricatorUserOAuthInfo' => 'applications/people/storage/useroauthinfo',
'PhabricatorUserProfile' => 'applications/people/storage/profile',
'PhabricatorUserSettingsController' => 'applications/people/controller/settings',
'PhabricatorXHProfController' => 'applications/xhprof/controller/base',
@@ -436,8 +441,6 @@ phutil_register_library_map(array(
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
'PhabricatorEmailTokenController' => 'PhabricatorAuthController',
- 'PhabricatorFacebookAuthController' => 'PhabricatorAuthController',
- 'PhabricatorFacebookAuthDiagnosticsController' => 'PhabricatorAuthController',
'PhabricatorFile' => 'PhabricatorFileDAO',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
@@ -459,6 +462,11 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
+ 'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController',
+ 'PhabricatorOAuthFailureView' => 'AphrontView',
+ 'PhabricatorOAuthLoginController' => 'PhabricatorAuthController',
+ 'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider',
+ 'PhabricatorOAuthProviderGithub' => 'PhabricatorOAuthProvider',
'PhabricatorPHID' => 'PhabricatorPHIDDAO',
'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
'PhabricatorPHIDController' => 'PhabricatorController',
@@ -507,6 +515,7 @@ phutil_register_library_map(array(
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorUser' => 'PhabricatorUserDAO',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
+ 'PhabricatorUserOAuthInfo' => 'PhabricatorUserDAO',
'PhabricatorUserProfile' => 'PhabricatorUserDAO',
'PhabricatorUserSettingsController' => 'PhabricatorPeopleController',
'PhabricatorXHProfController' => 'PhabricatorController',
diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
index 5e3b169bcb..3b3dee2f44 100644
--- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -130,6 +130,13 @@ class AphrontDefaultApplicationConfiguration
'diagnose/$' => 'PhabricatorFacebookAuthDiagnosticsController',
),
+ '/oauth/' => array(
+ '(?Pgithub|facebook)/' => array(
+ 'login/$' => 'PhabricatorOAuthLoginController',
+ 'diagnose/$' => 'PhabricatorOAuthDiagnosticsController',
+ ),
+ ),
+
'/xhprof/' => array(
'profile/(?P[^/]+)/$' => 'PhabricatorXHProfProfileController',
),
diff --git a/src/applications/auth/controller/facebookauth/PhabricatorFacebookAuthController.php b/src/applications/auth/controller/facebookauth/PhabricatorFacebookAuthController.php
deleted file mode 100644
index ab23fefbb7..0000000000
--- a/src/applications/auth/controller/facebookauth/PhabricatorFacebookAuthController.php
+++ /dev/null
@@ -1,293 +0,0 @@
-'.
- 'Diagnose Facebook Auth Problems'.
- '';
-
- $request = $this->getRequest();
-
- if ($request->getStr('error')) {
- $view = new AphrontRequestFailureView();
- $view->setHeader('Facebook Auth Failed');
- $view->appendChild(
- ''.
- 'Description: '.
- phutil_escape_html($request->getStr('error_description')).
- '
');
- $view->appendChild(
- ''.
- 'Error: '.
- phutil_escape_html($request->getStr('error')).
- '
');
- $view->appendChild(
- ''.
- 'Error Reason: '.
- phutil_escape_html($request->getStr('error_reason')).
- '
');
- $view->appendChild(
- '');
-
- return $this->buildStandardPageResponse(
- $view,
- array(
- 'title' => 'Facebook Auth Failed',
- ));
- }
-
- $token = $request->getStr('token');
- if (!$token) {
- $app_id = PhabricatorEnv::getEnvConfig('facebook.application-id');
- $app_secret = PhabricatorEnv::getEnvConfig('facebook.application-secret');
- $redirect_uri = PhabricatorEnv::getURI('/facebook-auth/');
-
- $code = $request->getStr('code');
- $auth_uri = new PhutilURI(
- "https://graph.facebook.com/oauth/access_token");
- $auth_uri->setQueryParams(
- array(
- 'client_id' => $app_id,
- 'redirect_uri' => $redirect_uri,
- 'client_secret' => $app_secret,
- 'code' => $code,
- ));
-
- $response = @file_get_contents($auth_uri);
- if ($response === false) {
- $view = new AphrontRequestFailureView();
- $view->setHeader('Facebook Auth Failed');
- $view->appendChild(
- 'Unable to authenticate with Facebook. There are several reasons '.
- 'this might happen:
'.
- ''.
- '- Phabricator may be configured with the wrong Application '.
- 'Secret; or
'.
- '- the Facebook OAuth access token may have expired; or
'.
- '- Facebook may have revoked authorization for the '.
- 'Application; or
'.
- '- Facebook may be having technical problems.
'.
- '
'.
- 'You can try again, or login using another method.
');
- $view->appendChild(
- '');
-
- return $this->buildStandardPageResponse(
- $view,
- array(
- 'title' => 'Facebook Auth Failed',
- ));
- }
-
- $data = array();
- parse_str($response, $data);
-
- $token = $data['access_token'];
- }
-
- $user_json = @file_get_contents('https://graph.facebook.com/me?access_token='.$token);
- $user_data = json_decode($user_json, true);
-
- $user_id = $user_data['id'];
-
- $known_user = id(new PhabricatorUser())
- ->loadOneWhere('facebookUID = %d', $user_id);
- if ($known_user) {
- $session_key = $known_user->establishSession('web');
- $request->setCookie('phusr', $known_user->getUsername());
- $request->setCookie('phsid', $session_key);
- return id(new AphrontRedirectResponse())
- ->setURI('/');
- }
-
- $known_email = id(new PhabricatorUser())
- ->loadOneWhere('email = %s', $user_data['email']);
- if ($known_email) {
- if ($known_email->getFacebookUID()) {
- throw new Exception(
- "The email associated with the Facebook account you just logged in ".
- "with is already associated with another Phabricator account which ".
- "is, in turn, associated with a Facebook account different from ".
- "the one you just logged in with.");
- }
- $known_email->setFacebookUID($user_id);
- $session_key = $known_email->establishSession('web');
- $request->setCookie('phusr', $known_email->getUsername());
- $request->setCookie('phsid', $session_key);
- return id(new AphrontRedirectResponse())
- ->setURI('/');
- }
-
- $current_user = $this->getRequest()->getUser();
- if ($current_user->getPHID()) {
- if ($current_user->getFacebookUID() &&
- $current_user->getFacebookUID() != $user_id) {
- throw new Exception(
- "Your account is already associated with a Facebook user ID other ".
- "than the one you just logged in with...?");
- }
-
- if ($request->isFormPost()) {
- $current_user->setFacebookUID($user_id);
- $current_user->save();
-
- // TODO: ship them back to the 'account' page or whatever?
- return id(new AphrontRedirectResponse())
- ->setURI('/');
- }
-
- $ph_account = $current_user->getUsername();
- $fb_account = phutil_escape_html($user_data['name']);
-
- $form = new AphrontFormView();
- $form
- ->addHiddenInput('token', $token)
- ->setUser($request->getUser())
- ->setAction('/facebook-auth/')
- ->appendChild(
- 'Do you want to link your '.
- "existing Phabricator account ({$ph_account}) ".
- "with your Facebook account ({$fb_account}) so ".
- "you can login with Facebook?")
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->setValue('Link Accounts')
- ->addCancelButton('/login/'));
-
- $panel = new AphrontPanelView();
- $panel->setHeader('Link Facebook Account');
- $panel->setWidth(AphrontPanelView::WIDTH_FORM);
- $panel->appendChild($form);
-
- return $this->buildStandardPageResponse(
- $panel,
- array(
- 'title' => 'Link Facebook Account',
- ));
- }
-
- $errors = array();
- $e_username = true;
-
- $user = new PhabricatorUser();
-
- $matches = null;
- if (preg_match('@/([a-zA-Z0-9]+)$@', $user_data['link'], $matches)) {
- $user->setUsername($matches[1]);
- }
-
- if ($request->isFormPost()) {
-
- $username = $request->getStr('username');
- if (!strlen($username)) {
- $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.';
- }
-
- $user->setUsername($username);
- $user->setFacebookUID($user_id);
- $user->setEmail($user_data['email']);
-
- if (!$errors) {
- $image = @file_get_contents('https://graph.facebook.com/me/picture?access_token='.$token);
- $file = PhabricatorFile::newFromFileData(
- $image,
- array(
- 'name' => 'fbprofile.jpg'
- ));
-
- $user->setProfileImagePHID($file->getPHID());
- $user->setRealName($user_data['name']);
-
- try {
- $user->save();
-
- $session_key = $user->establishSession('web');
- $request->setCookie('phusr', $user->getUsername());
- $request->setCookie('phsid', $session_key);
- return id(new AphrontRedirectResponse())->setURI('/');
- } catch (AphrontQueryDuplicateKeyException $exception) {
- $key = $exception->getDuplicateKey();
- if ($key == 'userName') {
- $e_username = 'Duplicate';
- $errors[] = 'That username is not unique.';
- } else {
- throw $exception;
- }
- }
- }
- }
-
- $error_view = null;
- if ($errors) {
- $error_view = new AphrontErrorView();
- $error_view->setTitle('Facebook Auth Failed');
- $error_view->setErrors($errors);
- }
-
- $form = new AphrontFormView();
- $form
- ->addHiddenInput('token', $token)
- ->setUser($request->getUser())
- ->setAction('/facebook-auth/')
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel('Username')
- ->setName('username')
- ->setValue($user->getUsername())
- ->setError($e_username))
- ->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/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php
index 774a26d7e9..57979f3fde 100644
--- a/src/applications/auth/controller/login/PhabricatorLoginController.php
+++ b/src/applications/auth/controller/login/PhabricatorLoginController.php
@@ -88,38 +88,47 @@ class PhabricatorLoginController extends PhabricatorAuthController {
// $panel->setCreateButton('Register New Account', '/login/register/');
$panel->appendChild($form);
- $fbauth_enabled = PhabricatorEnv::getEnvConfig('facebook.auth-enabled');
- if ($fbauth_enabled) {
- $auth_uri = new PhutilURI("https://www.facebook.com/dialog/oauth");
+ $providers = array(
+ PhabricatorOAuthProvider::PROVIDER_FACEBOOK,
+ PhabricatorOAuthProvider::PROVIDER_GITHUB,
+ );
+ foreach ($providers as $provider_key) {
+ $provider = PhabricatorOAuthProvider::newProvider($provider_key);
- $user = $request->getUser();
+ $enabled = $provider->isProviderEnabled();
+ if (!$enabled) {
+ continue;
+ }
- $redirect_uri = PhabricatorEnv::getURI('/facebook-auth/');
- $app_id = PhabricatorEnv::getEnvConfig('facebook.application-id');
+ $auth_uri = $provider->getAuthURI();
+ $redirect_uri = $provider->getRedirectURI();
+ $client_id = $provider->getClientID();
+ $provider_name = $provider->getProviderName();
// TODO: In theory we should use 'state' to prevent CSRF, but the total
// effect of the CSRF attack is that an attacker can cause a user to login
- // to Phabricator if they're already logged into Facebook. This does not
- // seem like the most severe threat in the world, and generating CSRF for
- // logged-out users is vaugely tricky.
+ // to Phabricator if they're already logged into some OAuth provider. This
+ // does not seem like the most severe threat in the world, and generating
+ // CSRF for logged-out users is vaugely tricky.
- $facebook_auth = new AphrontFormView();
- $facebook_auth
+ $auth_form = new AphrontFormView();
+ $auth_form
->setAction($auth_uri)
- ->addHiddenInput('client_id', $app_id)
+ ->addHiddenInput('client_id', $client_id)
->addHiddenInput('redirect_uri', $redirect_uri)
- ->addHiddenInput('scope', 'email')
->setUser($request->getUser())
->setMethod('GET')
->appendChild(
'
Login or register for '.
- 'Phabricator using your Facebook account.
')
+ 'Phabricator using your '.$provider_name.' account.
')
->appendChild(
id(new AphrontFormSubmitControl())
- ->setValue("Login with Facebook \xC2\xBB"));
+ ->setValue("Login with {$provider_name} \xC2\xBB"));
- $panel->appendChild('
Login or Register with Facebook
');
- $panel->appendChild($facebook_auth);
+ $panel->appendChild(
+ '
Login or Register with '.$provider_name.'
');
+
+ $panel->appendChild($auth_form);
}
return $this->buildStandardPageResponse(
diff --git a/src/applications/auth/controller/login/__init__.php b/src/applications/auth/controller/login/__init__.php
index 85ec5fcd3e..532ff90b92 100644
--- a/src/applications/auth/controller/login/__init__.php
+++ b/src/applications/auth/controller/login/__init__.php
@@ -8,14 +8,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/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');
phutil_require_module('phabricator', 'view/layout/panel');
-phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php
new file mode 100644
index 0000000000..d84e266de6
--- /dev/null
+++ b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php
@@ -0,0 +1,371 @@
+provider = PhabricatorOAuthProvider::newProvider($data['provider']);
+ }
+
+ public function processRequest() {
+ $current_user = $this->getRequest()->getUser();
+ if ($current_user->getPHID()) {
+ // If we're already logged in, ignore everything going on here. TODO:
+ // restore account linking.
+ return id(new AphrontRedirectResponse())->setURI('/');
+ }
+
+ $provider = $this->provider;
+ if (!$provider->isProviderEnabled()) {
+ return new Aphront400Response();
+ }
+
+ $request = $this->getRequest();
+
+ if ($request->getStr('error')) {
+ $error_view = id(new PhabricatorOAuthFailureView())
+ ->setRequest($request);
+ 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,
+ );
+
+ $stream_context = stream_context_create(
+ array(
+ 'http' => array(
+ 'method' => 'POST',
+ 'header' => 'Content-type: application/x-www-form-urlencoded',
+ 'content' => http_build_query($query_data),
+ ),
+ ));
+
+ $stream = fopen($auth_uri, 'r', false, $stream_context);
+
+ $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());
+ }
+ }
+
+ $userinfo_uri = new PhutilURI($provider->getUserInfoURI());
+ $userinfo_uri->setQueryParams(
+ array(
+ 'access_token' => $token,
+ ));
+
+ $user_json = @file_get_contents($userinfo_uri);
+ $user_data = json_decode($user_json, true);
+
+ $this->accessToken = $token;
+
+ switch ($provider->getProviderKey()) {
+ case PhabricatorOAuthProvider::PROVIDER_GITHUB:
+ $user_data = $user_data['user'];
+ break;
+ }
+ $this->userData = $user_data;
+
+ $user_id = $this->retrieveUserID();
+
+ // Login with known auth.
+
+ $known_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
+ 'oauthProvider = %s and oauthUID = %s',
+ $provider->getProviderKey(),
+ $user_id);
+ if ($known_oauth) {
+ $known_user = id(new PhabricatorUser())->load($known_oauth->getUserID());
+ $session_key = $known_user->establishSession('web');
+ $request->setCookie('phusr', $known_user->getUsername());
+ $request->setCookie('phsid', $session_key);
+ return id(new AphrontRedirectResponse())
+ ->setURI('/');
+ }
+
+ // Merge accounts based on shared email. TODO: should probably get rid of
+ // this.
+
+ $oauth_email = $this->retrieveUserEmail();
+ if ($oauth_email) {
+ $known_email = id(new PhabricatorUser())
+ ->loadOneWhere('email = %s', $oauth_email);
+ if ($known_email) {
+ $known_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
+ 'userID = %d AND oauthProvider = %s',
+ $known_email->getID(),
+ $provider->getProviderKey());
+ if ($known_oauth) {
+ $provider_name = $provider->getName();
+ throw new Exception(
+ "The email associated with the ".$provider_name." account you ".
+ "just logged in with is already associated with another ".
+ "Phabricator account which is, in turn, associated with a ".
+ $provider_name." account different from the one you just logged ".
+ "in with.");
+ }
+
+ $oauth_info = new PhabricatorUserOAuthInfo();
+ $oauth_info->setUserID($known_email->getID());
+ $oauth_info->setOAuthProvider($provider->getProviderKey());
+ $oauth_info->setOAuthUID($user_id);
+ $oauth_info->save();
+
+ $session_key = $known_email->establishSession('web');
+ $request->setCookie('phusr', $known_email->getUsername());
+ $request->setCookie('phsid', $session_key);
+ return id(new AphrontRedirectResponse())
+ ->setURI('/');
+ }
+ }
+
+ $errors = array();
+ $e_username = true;
+ $e_email = true;
+ $e_realname = true;
+
+ $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());
+ $oauth_info->setOAuthProvider($provider->getProviderKey());
+ $oauth_info->setOAuthUID($user_id);
+ $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) {
+ $key = $exception->getDuplicateKey();
+ if ($key == 'userName') {
+ $e_username = 'Duplicate';
+ $errors[] = 'That username is not unique.';
+ } else if ($key == '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', $token)
+ ->setUser($request->getUser())
+ ->setAction($provider->getRedirectURI())
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Username')
+ ->setName('username')
+ ->setValue($user->getUsername())
+ ->setError($e_username));
+
+ 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',
+ ));
+ }
+
+ private function buildErrorResponse(PhabricatorOAuthFailureView $view) {
+ $provider = $this->provider;
+
+ $provider_name = $provider->getProviderName();
+ $view->setOAuthProvider($provider);
+
+ return $this->buildStandardPageResponse(
+ $view,
+ array(
+ 'title' => $provider_name.' Auth Failed',
+ ));
+ }
+
+ private function retrieveUserID() {
+ return $this->userData['id'];
+ }
+
+ 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'];
+ }
+ 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);
+ }
+ }
+ return null;
+ }
+
+ private function retreiveRealNameSuggestion() {
+ return $this->userData['name'];
+ }
+
+}
diff --git a/src/applications/auth/controller/facebookauth/__init__.php b/src/applications/auth/controller/oauth/__init__.php
similarity index 68%
rename from src/applications/auth/controller/facebookauth/__init__.php
rename to src/applications/auth/controller/oauth/__init__.php
index f5b4a8f1a9..d9e4cc1b35 100644
--- a/src/applications/auth/controller/facebookauth/__init__.php
+++ b/src/applications/auth/controller/oauth/__init__.php
@@ -9,17 +9,19 @@
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/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', 'infrastructure/env');
+phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo');
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('phabricator', 'view/page/failure');
-phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils');
-phutil_require_source('PhabricatorFacebookAuthController.php');
+
+phutil_require_source('PhabricatorOAuthLoginController.php');
diff --git a/src/applications/auth/controller/facebookauth/diagnostics/PhabricatorFacebookAuthDiagnosticsController.php b/src/applications/auth/controller/oauthdiagnostics/PhabricatorOAuthDiagnosticsController.php
similarity index 92%
rename from src/applications/auth/controller/facebookauth/diagnostics/PhabricatorFacebookAuthDiagnosticsController.php
rename to src/applications/auth/controller/oauthdiagnostics/PhabricatorOAuthDiagnosticsController.php
index f880e6eadf..713500b219 100644
--- a/src/applications/auth/controller/facebookauth/diagnostics/PhabricatorFacebookAuthDiagnosticsController.php
+++ b/src/applications/auth/controller/oauthdiagnostics/PhabricatorOAuthDiagnosticsController.php
@@ -16,18 +16,26 @@
* limitations under the License.
*/
-class PhabricatorFacebookAuthDiagnosticsController
+class PhabricatorOAuthDiagnosticsController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
+ public function willProcessRequest(array $data) {
+ $this->provider = PhabricatorOAuthProvider::newProvider($data['provider']);
+ }
+
public function processRequest() {
- $auth_enabled = PhabricatorEnv::getEnvConfig('facebook.auth-enabled');
- $app_id = PhabricatorEnv::getEnvConfig('facebook.application-id');
- $app_secret = PhabricatorEnv::getEnvConfig('facebook.application-secret');
+ $provider = $this->provider;
+
+
+
+ $auth_enabled = $provider->isProviderEnabled();
+ $client_id = $provider->getClientID();
+ $client_secret = $provider->getClientSecret();
$res_ok = 'OK';
$res_no = 'NO';
@@ -48,7 +56,7 @@ class PhabricatorFacebookAuthDiagnosticsController
'Facebook authentication is enabled.');
}
- if (!$app_id) {
+ if (!$client_id) {
$results['facebook.application-id'] = array(
$res_no,
null,
@@ -60,11 +68,11 @@ class PhabricatorFacebookAuthDiagnosticsController
} else {
$results['facebook.application-id'] = array(
$res_ok,
- $app_id,
+ $client_id,
'Application ID is set.');
}
- if (!$app_secret) {
+ if (!$client_secret) {
$results['facebook.application-secret'] = array(
$res_no,
null,
@@ -141,8 +149,8 @@ class PhabricatorFacebookAuthDiagnosticsController
$test_uri = new PhutilURI('https://graph.facebook.com/oauth/access_token');
$test_uri->setQueryParams(
array(
- 'client_id' => $app_id,
- 'client_secret' => $app_secret,
+ 'client_id' => $client_id,
+ 'client_secret' => $client_secret,
'grant_type' => 'client_credentials',
));
diff --git a/src/applications/auth/controller/facebookauth/diagnostics/__init__.php b/src/applications/auth/controller/oauthdiagnostics/__init__.php
similarity index 72%
rename from src/applications/auth/controller/facebookauth/diagnostics/__init__.php
rename to src/applications/auth/controller/oauthdiagnostics/__init__.php
index 4603969d31..bebfb1718c 100644
--- a/src/applications/auth/controller/facebookauth/diagnostics/__init__.php
+++ b/src/applications/auth/controller/oauthdiagnostics/__init__.php
@@ -7,7 +7,7 @@
phutil_require_module('phabricator', 'applications/auth/controller/base');
-phutil_require_module('phabricator', 'infrastructure/env');
+phutil_require_module('phabricator', 'applications/auth/oauth/provider/base');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/layout/panel');
@@ -15,4 +15,4 @@ phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'parser/uri');
-phutil_require_source('PhabricatorFacebookAuthDiagnosticsController.php');
+phutil_require_source('PhabricatorOAuthDiagnosticsController.php');
diff --git a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php
new file mode 100644
index 0000000000..5fc95fed41
--- /dev/null
+++ b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php
@@ -0,0 +1,65 @@
+request = $request;
+ return $this;
+ }
+
+ public function setOAuthProvider($provider) {
+ $this->provider = $provider;
+ return $this;
+ }
+
+ public function render() {
+ $request = $this->request;
+ $provider = $this->provider;
+ $provider_name = $provider->getProviderName();
+
+ $diagnose = null;
+
+ $view = new AphrontRequestFailureView();
+ $view->setHeader($provider_name.' Auth Failed');
+ if ($this->request) {
+ $view->appendChild(
+ ''.
+ 'Description: '.
+ phutil_escape_html($request->getStr('error_description')).
+ '
');
+ $view->appendChild(
+ ''.
+ 'Error: '.
+ phutil_escape_html($request->getStr('error')).
+ '
');
+ $view->appendChild(
+ ''.
+ 'Error Reason: '.
+ phutil_escape_html($request->getStr('error_reason')).
+ '
');
+ } else {
+ // TODO: We can probably refine this.
+ $view->appendChild(
+ 'Unable to authenticate with '.$provider_name.'. '.
+ 'There are several reasons this might happen:
'.
+ ''.
+ '- Phabricator may be configured with the wrong Application '.
+ 'Secret; or
'.
+ '- the '.$provider_name.' OAuth access token may have expired; '.
+ 'or
'.
+ '- '.$provider_name.' may have revoked authorization for the '.
+ 'Application; or
'.
+ '- '.$provider_name.' may be having technical problems.
'.
+ '
'.
+ 'You can try again, or login using another method.
');
+
+ $provider_key = $provider->getProviderKey();
+ $diagnose =
+ ''.
+ 'Diagnose '.$provider_name.' OAuth Problems'.
+ '';
+ }
+
+ $view->appendChild(
+ '');
+
+ return $view->render();
+ }
+
+}
diff --git a/src/applications/auth/view/oauthfailure/__init__.php b/src/applications/auth/view/oauthfailure/__init__.php
new file mode 100644
index 0000000000..d94eeeaf2d
--- /dev/null
+++ b/src/applications/auth/view/oauthfailure/__init__.php
@@ -0,0 +1,15 @@
+