mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 14:52:41 +01:00
Add Google as an OAuth2 provider (BETA)
Summary: This is pretty straightforward, except: - We need to request read/write access to the address book to get the account ID (which we MUST have) and real name, email and account name (which we'd like to have). This is way more access than we should need, but there's apparently no "get_loggedin_user_basic_information" type of call in the Google API suite (or, at least, I couldn't find one). - We can't get the profile picture or profile URI since there's no Plus API access and Google users don't have meaningful public pages otherwise. - Google doesn't save the fact that you've authorized the app, so every time you want to login you need to reaffirm that you want to give us silly amounts of access. Phabricator sessions are pretty long-duration though so this shouldn't be a major issue. Test Plan: - Registered, logged out, and logged in with Google. - Registered, logged out, and logged in with Facebook / Github to make sure I didn't break anything. - Linked / unlinked Google accounts. Reviewers: Makinde, jungejason, nh, tuomaspelkonen, aran Reviewed By: aran CC: aran, epriestley, Makinde Differential Revision: 916
This commit is contained in:
parent
4da43b31a3
commit
1620bce842
12 changed files with 263 additions and 20 deletions
|
@ -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.
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -138,7 +138,7 @@ class AphrontDefaultApplicationConfiguration
|
|||
'/logout/$' => 'PhabricatorLogoutController',
|
||||
|
||||
'/oauth/' => array(
|
||||
'(?P<provider>github|facebook)/' => array(
|
||||
'(?P<provider>\w+)/' => array(
|
||||
'login/$' => 'PhabricatorOAuthLoginController',
|
||||
'diagnose/$' => 'PhabricatorOAuthDiagnosticsController',
|
||||
'unlink/$' => 'PhabricatorOAuthUnlinkController',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -69,7 +69,7 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider {
|
|||
}
|
||||
|
||||
public function setUserData($data) {
|
||||
$this->userData = $data;
|
||||
$this->userData = json_decode($data, true);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class PhabricatorOAuthProviderGoogle extends PhabricatorOAuthProvider {
|
||||
|
||||
private $userData;
|
||||
|
||||
public function getProviderKey() {
|
||||
return self::PROVIDER_GOOGLE;
|
||||
}
|
||||
|
||||
public function getProviderName() {
|
||||
return 'Google (BETA)';
|
||||
}
|
||||
|
||||
public function isProviderEnabled() {
|
||||
return PhabricatorEnv::getEnvConfig('google.auth-enabled');
|
||||
}
|
||||
|
||||
public function isProviderLinkPermanent() {
|
||||
return PhabricatorEnv::getEnvConfig('google.auth-permanent');
|
||||
}
|
||||
|
||||
public function isProviderRegistrationEnabled() {
|
||||
return PhabricatorEnv::getEnvConfig('google.registration-enabled');
|
||||
}
|
||||
|
||||
public function getRedirectURI() {
|
||||
return PhabricatorEnv::getURI('/oauth/google/login/');
|
||||
}
|
||||
|
||||
public function getClientID() {
|
||||
return PhabricatorEnv::getEnvConfig('google.application-id');
|
||||
}
|
||||
|
||||
public function getClientSecret() {
|
||||
return PhabricatorEnv::getEnvConfig('google.application-secret');
|
||||
}
|
||||
|
||||
public function getAuthURI() {
|
||||
return 'https://accounts.google.com/o/oauth2/auth';
|
||||
}
|
||||
|
||||
public function getTokenURI() {
|
||||
return 'https://accounts.google.com/o/oauth2/token';
|
||||
}
|
||||
|
||||
public function getUserInfoURI() {
|
||||
return 'https://www.google.com/m8/feeds/contacts/default/full';
|
||||
}
|
||||
|
||||
public function getMinimumScope() {
|
||||
// This is the Google contacts API, which is apparently the best way to get
|
||||
// the user ID / login / email since Google doesn't apparently have a
|
||||
// more generic "user.info" sort of call (or, if it does, I couldn't find
|
||||
// it). This is sort of terrifying since it lets Phabricator read your whole
|
||||
// address book and possibly your physical address and such, so it would
|
||||
// be really nice to find a way to restrict this scope to something less
|
||||
// crazily permissive. But users will click anything and the dialog isn't
|
||||
// very scary, so whatever.
|
||||
return 'https://www.google.com/m8/feeds';
|
||||
}
|
||||
|
||||
public function setUserData($data) {
|
||||
$xml = new SimpleXMLElement($data);
|
||||
$id = (string)$xml->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);
|
||||
}
|
||||
|
||||
}
|
15
src/applications/auth/oauth/provider/google/__init__.php
Normal file
15
src/applications/auth/oauth/provider/google/__init__.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'applications/auth/oauth/provider/base');
|
||||
phutil_require_module('phabricator', 'infrastructure/env');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorOAuthProviderGoogle.php');
|
|
@ -68,12 +68,20 @@ class PhabricatorUserOAuthSettingsPanelController
|
|||
$auth_uri = $provider->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"));
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue