diff --git a/conf/default.conf.php b/conf/default.conf.php index ce649a97e7..ef8575ad0a 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -335,6 +335,24 @@ return array( 'github.application-secret' => null, +// -- Google ---------------------------------------------------------------- // + + // Can users use Google credentials to login to Phabricator? + 'google.auth-enabled' => false, + + // Can users use Google credentials to create new Phabricator accounts? + 'google.registration-enabled' => true, + + // Are Google accounts permanently linked to Phabricator accounts, or can + // the user unlink them? + 'google.auth-permanent' => false, + + // The Google "Client ID" to use for Google API access. + 'google.application-id' => null, + + // The Google "Client Secret" to use for Google API access. + 'google.application-secret' => null, + // -- Recaptcha ------------------------------------------------------------- // // Is Recaptcha enabled? If disabled, captchas will not appear. diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ea1f67ae29..6069d0fb18 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -486,6 +486,7 @@ phutil_register_library_map(array( 'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/base', 'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/facebook', 'PhabricatorOAuthProviderGithub' => 'applications/auth/oauth/provider/github', + 'PhabricatorOAuthProviderGoogle' => 'applications/auth/oauth/provider/google', 'PhabricatorOAuthRegistrationController' => 'applications/auth/controller/oauthregistration/base', 'PhabricatorOAuthUnlinkController' => 'applications/auth/controller/unlink', 'PhabricatorObjectGraph' => 'applications/phid/graph', @@ -1090,6 +1091,7 @@ phutil_register_library_map(array( 'PhabricatorOAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderGithub' => 'PhabricatorOAuthProvider', + 'PhabricatorOAuthProviderGoogle' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthRegistrationController' => 'PhabricatorAuthController', 'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorObjectGraph' => 'AbstractDirectedGraph', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index eecef65cd6..b73dfefd99 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -138,7 +138,7 @@ class AphrontDefaultApplicationConfiguration '/logout/$' => 'PhabricatorLogoutController', '/oauth/' => array( - '(?Pgithub|facebook)/' => array( + '(?P\w+)/' => array( 'login/$' => 'PhabricatorOAuthLoginController', 'diagnose/$' => 'PhabricatorOAuthDiagnosticsController', 'unlink/$' => 'PhabricatorOAuthUnlinkController', diff --git a/src/applications/auth/controller/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php index 6980cac839..3d648f125f 100644 --- a/src/applications/auth/controller/login/PhabricatorLoginController.php +++ b/src/applications/auth/controller/login/PhabricatorLoginController.php @@ -121,13 +121,8 @@ class PhabricatorLoginController extends PhabricatorAuthController { $forms['Phabricator Login'] = $form; } - $providers = array( - PhabricatorOAuthProvider::PROVIDER_FACEBOOK, - PhabricatorOAuthProvider::PROVIDER_GITHUB, - ); - foreach ($providers as $provider_key) { - $provider = PhabricatorOAuthProvider::newProvider($provider_key); - + $providers = PhabricatorOAuthProvider::getAllProviders(); + foreach ($providers as $provider) { $enabled = $provider->isProviderEnabled(); if (!$enabled) { continue; @@ -138,6 +133,7 @@ class PhabricatorLoginController extends PhabricatorAuthController { $client_id = $provider->getClientID(); $provider_name = $provider->getProviderName(); $minimum_scope = $provider->getMinimumScope(); + $extra_auth = $provider->getExtraAuthParameters(); // 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 @@ -163,7 +159,13 @@ class PhabricatorLoginController extends PhabricatorAuthController { ->setAction($auth_uri) ->addHiddenInput('client_id', $client_id) ->addHiddenInput('redirect_uri', $redirect_uri) - ->addHiddenInput('scope', $minimum_scope) + ->addHiddenInput('scope', $minimum_scope); + + foreach ($extra_auth as $key => $value) { + $auth_form->addHiddenInput($key, $value); + } + + $auth_form ->setUser($request->getUser()) ->setMethod('GET') ->appendChild( diff --git a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php index ceefaae846..c21ec54696 100644 --- a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php +++ b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php @@ -63,9 +63,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { 'access_token' => $this->accessToken, )); - $user_json = @file_get_contents($userinfo_uri); - $user_data = json_decode($user_json, true); - + $user_data = @file_get_contents($userinfo_uri); $provider->setUserData($user_data); $provider->setAccessToken($this->accessToken); @@ -240,7 +238,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { 'client_secret' => $client_secret, 'redirect_uri' => $redirect_uri, 'code' => $code, - ); + ) + $provider->getExtraTokenParameters(); $post_data = http_build_query($query_data); $post_length = strlen($post_data); @@ -270,8 +268,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); } - $data = array(); - parse_str($response, $data); + $data = $provider->decodeTokenResponse($response); $token = idx($data, 'access_token'); if (!$token) { diff --git a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php index f5124eaf81..a2065959eb 100644 --- a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php +++ b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php @@ -20,6 +20,7 @@ abstract class PhabricatorOAuthProvider { const PROVIDER_FACEBOOK = 'facebook'; const PROVIDER_GITHUB = 'github'; + const PROVIDER_GOOGLE = 'google'; private $accessToken; @@ -32,7 +33,25 @@ abstract class PhabricatorOAuthProvider { abstract public function getClientID(); abstract public function getClientSecret(); abstract public function getAuthURI(); + + /** + * If the provider needs extra stuff in the auth request, return it here. + * For example, Google needs a response_type parameter. + */ + public function getExtraAuthParameters() { + return array(); + } + abstract public function getTokenURI(); + + /** + * If the provider needs extra stuff in the token request, return it here. + * For example, Google needs a grant_type parameter. + */ + public function getExtraTokenParameters() { + return array(); + } + abstract public function getUserInfoURI(); abstract public function getMinimumScope(); @@ -44,10 +63,21 @@ abstract class PhabricatorOAuthProvider { abstract public function retrieveUserAccountURI(); abstract public function retrieveUserRealName(); + /** + * Override this if the provider returns the token response as, e.g., JSON + * or XML. + */ + public function decodeTokenResponse($response) { + $data = null; + parse_str($response, $data); + return $data; + } + public function __construct() { } + final public function setAccessToken($access_token) { $this->accessToken = $access_token; return $this; @@ -65,6 +95,9 @@ abstract class PhabricatorOAuthProvider { case self::PROVIDER_GITHUB: $class = 'PhabricatorOAuthProviderGithub'; break; + case self::PROVIDER_GOOGLE: + $class = 'PhabricatorOAuthProviderGoogle'; + break; default: throw new Exception('Unknown OAuth provider.'); } @@ -76,6 +109,7 @@ abstract class PhabricatorOAuthProvider { $all = array( self::PROVIDER_FACEBOOK, self::PROVIDER_GITHUB, + self::PROVIDER_GOOGLE, ); $providers = array(); foreach ($all as $provider) { diff --git a/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php b/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php index ec97334a1f..aa305fe09f 100644 --- a/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php +++ b/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php @@ -69,7 +69,7 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { } public function setUserData($data) { - $this->userData = $data; + $this->userData = json_decode($data, true); return $this; } diff --git a/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php b/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php index f6e9759d19..e41d7a76f3 100644 --- a/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php +++ b/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php @@ -69,7 +69,7 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { } public function setUserData($data) { - $this->userData = $data['user']; + $this->userData = idx(json_decode($data, true), 'user'); return $this; } diff --git a/src/applications/auth/oauth/provider/google/PhabricatorOAuthProviderGoogle.php b/src/applications/auth/oauth/provider/google/PhabricatorOAuthProviderGoogle.php new file mode 100644 index 0000000000..f9b3a57608 --- /dev/null +++ b/src/applications/auth/oauth/provider/google/PhabricatorOAuthProviderGoogle.php @@ -0,0 +1,136 @@ +id; + $this->userData = array( + 'id' => $id, + 'email' => (string)$xml->author[0]->email, + 'real' => (string)$xml->author[0]->name, + + // Guess account name from email address, this is just a hint anyway. + 'account' => head(explode('@', $id)), + ); + return $this; + } + + public function retrieveUserID() { + return $this->userData['id']; + } + + public function retrieveUserEmail() { + return $this->userData['email']; + } + + public function retrieveUserAccountName() { + return $this->userData['account']; + } + + public function retrieveUserProfileImage() { + // No apparent API access to Plus yet. + return null; + } + + public function retrieveUserAccountURI() { + // No apparent API access to Plus yet. + return null; + } + + public function retrieveUserRealName() { + return $this->userData['real']; + } + + public function getExtraAuthParameters() { + return array( + 'response_type' => 'code', + ); + } + + public function getExtraTokenParameters() { + return array( + 'grant_type' => 'authorization_code', + ); + + } + + public function decodeTokenResponse($response) { + return json_decode($response, true); + } + +} diff --git a/src/applications/auth/oauth/provider/google/__init__.php b/src/applications/auth/oauth/provider/google/__init__.php new file mode 100644 index 0000000000..92eeccc3bc --- /dev/null +++ b/src/applications/auth/oauth/provider/google/__init__.php @@ -0,0 +1,15 @@ +getAuthURI(); $client_id = $provider->getClientID(); $redirect_uri = $provider->getRedirectURI(); + $minimum_scope = $provider->getMinimumScope(); $form ->setAction($auth_uri) ->setMethod('GET') ->addHiddenInput('redirect_uri', $redirect_uri) ->addHiddenInput('client_id', $client_id) + ->addHiddenInput('scope', $minimum_scope); + + foreach ($provider->getExtraAuthParameters() as $key => $value) { + $form->addHiddenInput($key, $value); + } + + $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Link '.$provider_name." Account \xC2\xBB")); diff --git a/src/docs/configuration/configuring_accounts_and_registration.diviner b/src/docs/configuration/configuring_accounts_and_registration.diviner index b4c0cef3bb..a6b4096b2e 100644 --- a/src/docs/configuration/configuring_accounts_and_registration.diviner +++ b/src/docs/configuration/configuring_accounts_and_registration.diviner @@ -6,9 +6,9 @@ Describes how to configure user access to Phabricator. = Overview = Phabricator supports a number of login systems, like traditional -username/password, Facebook OAuth, and GitHub OAuth. You can enable or disable -these systems to configure who can register for and access your install, and -how users with existing accounts can login. +username/password, Facebook OAuth, GitHub OAuth, and Google OAuth. You can +enable or disable these systems to configure who can register for and access +your install, and how users with existing accounts can login. By default, only username/password auth is enabled, and there are no valid accounts. Start by creating a new account with the @@ -106,6 +106,37 @@ immediately clear how to get there via the UI: https://github.com/account/applications/ += Configuring Google OAuth = + +You can configure Google OAuth to allow login, login and registration, or +nothing (the default). + +To configure Google OAuth, create a new Google "API Project": + +https://code.google.com/apis/console/ + +You don't need to enable any **Services**, just go to **API Access**, click +**"Create an OAuth 2.0 client ID..."**, and configure these settings: + + - Click **More Options** next to **Authorized Redirect APIs** and add the + full domain (with protocol) plus ##/oauth/google/login/## to the list. + For example, ##https://phabricator.example.com/oauth/google/login/## + - Click **Create Client ID**. + +Once you've created a client ID, edit your Phabricator configuration and set +these keys: + + - **google.auth-enabled**: set this to ##true##. + - **google.application-id**: set this to your Client ID (from above). + - **google.application-secret**: set this to your Client Secret (from above). + - **google.registration-enabled**: set this to ##true## to let users register + with just Google credentials (this is a very open setting) or ##false## to + prevent users from registering. If set to ##false##, users may still link + existing accounts and use Google to login, they jus can't create new + accounts. + - **google.auth-permanent**: set this to ##true## to prevent unlinking + Phabricator accounts from Google accounts. + = Next Steps = Continue by: