mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-25 16:22:43 +01:00
OAuth -- add support for Disqus
Summary: also fix some bugs where we weren't properly capturing the expiry value or scope of access tokens. This code isn't the cleanest as some providers don't confirm what scope you've been granted. In that case, assume the access token is of the minimum scope Phabricator requires. This seems more useful to me as only Phabricator at the moment really easily / consistently lets the user increase / decrease the granted scope so its basically always the correct assumption at the time we make it. Test Plan: linked and unlinked Phabricator, Github, Disqus and Facebook accounts from Phabricator instaneces Reviewers: epriestley Reviewed By: epriestley CC: zeeg, aran, Koolvin Maniphest Tasks: T1110 Differential Revision: https://secure.phabricator.com/D2431
This commit is contained in:
parent
eb9645e9b4
commit
679f778235
11 changed files with 242 additions and 25 deletions
|
@ -146,6 +146,9 @@ return array(
|
|||
'phabricator.csrf-key',
|
||||
'facebook.application-secret',
|
||||
'github.application-secret',
|
||||
'google.application-secret',
|
||||
'phabricator.application-secret',
|
||||
'disqus.application-secret',
|
||||
'phabricator.mail-key',
|
||||
'security.hmac-key',
|
||||
),
|
||||
|
@ -512,6 +515,25 @@ return array(
|
|||
// The Google "Client Secret" to use for Google API access.
|
||||
'google.application-secret' => null,
|
||||
|
||||
// -- Disqus OAuth ---------------------------------------------------------- //
|
||||
|
||||
// Can users use Disqus credentials to login to Phabricator?
|
||||
'disqus.auth-enabled' => false,
|
||||
|
||||
// Can users use Disqus credentials to create new Phabricator accounts?
|
||||
'disqus.registration-enabled' => true,
|
||||
|
||||
// Are Disqus accounts permanently linked to Phabricator accounts, or can
|
||||
// the user unlink them?
|
||||
'disqus.auth-permanent' => false,
|
||||
|
||||
// The Disqus "Client ID" to use for Disqus API access.
|
||||
'disqus.application-id' => null,
|
||||
|
||||
// The Disqus "Client Secret" to use for Disqus API access.
|
||||
'disqus.application-secret' => null,
|
||||
|
||||
|
||||
// -- Phabricator OAuth ----------------------------------------------------- //
|
||||
|
||||
// Meta-town -- Phabricator is itself an OAuth Provider
|
||||
|
|
|
@ -731,6 +731,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOAuthFailureView' => 'applications/auth/view/oauthfailure',
|
||||
'PhabricatorOAuthLoginController' => 'applications/auth/controller/oauth',
|
||||
'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/base',
|
||||
'PhabricatorOAuthProviderDisqus' => 'applications/auth/oauth/provider/disqus',
|
||||
'PhabricatorOAuthProviderException' => 'applications/auth/oauth/provider/exception',
|
||||
'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/facebook',
|
||||
'PhabricatorOAuthProviderGitHub' => 'applications/auth/oauth/provider/github',
|
||||
|
@ -1646,6 +1647,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController',
|
||||
'PhabricatorOAuthFailureView' => 'AphrontView',
|
||||
'PhabricatorOAuthLoginController' => 'PhabricatorAuthController',
|
||||
'PhabricatorOAuthProviderDisqus' => 'PhabricatorOAuthProvider',
|
||||
'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider',
|
||||
'PhabricatorOAuthProviderGitHub' => 'PhabricatorOAuthProvider',
|
||||
'PhabricatorOAuthProviderGoogle' => 'PhabricatorOAuthProvider',
|
||||
|
|
|
@ -59,10 +59,7 @@ final class PhabricatorOAuthLoginController
|
|||
}
|
||||
|
||||
$userinfo_uri = new PhutilURI($provider->getUserInfoURI());
|
||||
$userinfo_uri->setQueryParams(
|
||||
array(
|
||||
'access_token' => $this->accessToken,
|
||||
));
|
||||
$userinfo_uri->setQueryParam('access_token', $this->accessToken);
|
||||
|
||||
try {
|
||||
$user_data = @file_get_contents($userinfo_uri);
|
||||
|
@ -129,9 +126,10 @@ final class PhabricatorOAuthLoginController
|
|||
hsprintf(
|
||||
'<p>Link your %s account to your Phabricator account?</p>',
|
||||
$provider_name));
|
||||
$dialog->addHiddenInput('token', $provider->getAccessToken());
|
||||
$dialog->addHiddenInput('confirm_token', $provider->getAccessToken());
|
||||
$dialog->addHiddenInput('expires', $oauth_info->getTokenExpires());
|
||||
$dialog->addHiddenInput('state', $this->oauthState);
|
||||
$dialog->addHiddenInput('scope', $oauth_info->getTokenScope());
|
||||
$dialog->addSubmitButton('Link Accounts');
|
||||
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
|
||||
|
||||
|
@ -238,18 +236,18 @@ final class PhabricatorOAuthLoginController
|
|||
private function retrieveAccessToken(PhabricatorOAuthProvider $provider) {
|
||||
$request = $this->getRequest();
|
||||
|
||||
$token = $request->getStr('token');
|
||||
$token = $request->getStr('confirm_token');
|
||||
if ($token) {
|
||||
$this->tokenExpires = $request->getInt('expires');
|
||||
$this->accessToken = $token;
|
||||
$this->oauthState = $request->getStr('state');
|
||||
$this->accessToken = $token;
|
||||
$this->oauthState = $request->getStr('state');
|
||||
return null;
|
||||
}
|
||||
|
||||
$client_id = $provider->getClientID();
|
||||
$client_secret = $provider->getClientSecret();
|
||||
$redirect_uri = $provider->getRedirectURI();
|
||||
$auth_uri = $provider->getTokenURI();
|
||||
$client_id = $provider->getClientID();
|
||||
$client_secret = $provider->getClientSecret();
|
||||
$redirect_uri = $provider->getRedirectURI();
|
||||
$auth_uri = $provider->getTokenURI();
|
||||
|
||||
$code = $request->getStr('code');
|
||||
$query_data = array(
|
||||
|
@ -294,12 +292,9 @@ final class PhabricatorOAuthLoginController
|
|||
return $this->buildErrorResponse(new PhabricatorOAuthFailureView());
|
||||
}
|
||||
|
||||
if (idx($data, 'expires')) {
|
||||
$this->tokenExpires = time() + $data['expires'];
|
||||
}
|
||||
|
||||
$this->accessToken = $token;
|
||||
$this->oauthState = $request->getStr('state');
|
||||
$this->tokenExpires = $provider->getTokenExpiryFromArray($data);
|
||||
$this->accessToken = $token;
|
||||
$this->oauthState = $request->getStr('state');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -311,16 +306,24 @@ final class PhabricatorOAuthLoginController
|
|||
$provider->getProviderKey(),
|
||||
$provider->retrieveUserID());
|
||||
|
||||
$scope = $this->getRequest()->getStr('scope');
|
||||
|
||||
if (!$oauth_info) {
|
||||
$oauth_info = new PhabricatorUserOAuthInfo();
|
||||
$oauth_info->setOAuthProvider($provider->getProviderKey());
|
||||
$oauth_info->setOAuthUID($provider->retrieveUserID());
|
||||
// some providers don't tell you what scope you got, so default
|
||||
// to the minimum Phabricator requires rather than assuming no scope
|
||||
if (!$scope) {
|
||||
$scope = $provider->getMinimumScope();
|
||||
}
|
||||
}
|
||||
|
||||
$oauth_info->setAccountURI($provider->retrieveUserAccountURI());
|
||||
$oauth_info->setAccountName($provider->retrieveUserAccountName());
|
||||
$oauth_info->setToken($provider->getAccessToken());
|
||||
$oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD);
|
||||
$oauth_info->setTokenScope($scope);
|
||||
|
||||
// If we have out-of-date expiration info, just clear it out. Then replace
|
||||
// it with good info if the provider gave it to us.
|
||||
|
@ -341,7 +344,4 @@ final class PhabricatorOAuthLoginController
|
|||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
$info->save();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ abstract class PhabricatorOAuthProvider {
|
|||
const PROVIDER_GITHUB = 'github';
|
||||
const PROVIDER_GOOGLE = 'google';
|
||||
const PROVIDER_PHABRICATOR = 'phabricator';
|
||||
const PROVIDER_DISQUS = 'disqus';
|
||||
|
||||
private $accessToken;
|
||||
|
||||
|
@ -55,6 +56,21 @@ abstract class PhabricatorOAuthProvider {
|
|||
|
||||
abstract public function getTokenURI();
|
||||
|
||||
/**
|
||||
* Access tokens expire based on an implementation-specific key.
|
||||
*/
|
||||
abstract protected function getTokenExpiryKey();
|
||||
public function getTokenExpiryFromArray(array $data) {
|
||||
$key = $this->getTokenExpiryKey();
|
||||
if ($key) {
|
||||
$expiry_value = idx($data, $key, 0);
|
||||
if ($expiry_value) {
|
||||
return time() + $expiry_value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provider needs extra stuff in the token request, return it here.
|
||||
* For example, Google needs a grant_type parameter.
|
||||
|
@ -133,6 +149,9 @@ abstract class PhabricatorOAuthProvider {
|
|||
case self::PROVIDER_PHABRICATOR:
|
||||
$class = 'PhabricatorOAuthProviderPhabricator';
|
||||
break;
|
||||
case self::PROVIDER_DISQUS:
|
||||
$class = 'PhabricatorOAuthProviderDisqus';
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Unknown OAuth provider.');
|
||||
}
|
||||
|
@ -146,6 +165,7 @@ abstract class PhabricatorOAuthProvider {
|
|||
self::PROVIDER_GITHUB,
|
||||
self::PROVIDER_GOOGLE,
|
||||
self::PROVIDER_PHABRICATOR,
|
||||
self::PROVIDER_DISQUS,
|
||||
);
|
||||
$providers = array();
|
||||
foreach ($all as $provider) {
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 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.
|
||||
*/
|
||||
|
||||
final class PhabricatorOAuthProviderDisqus extends PhabricatorOAuthProvider {
|
||||
|
||||
private $userData;
|
||||
|
||||
public function getProviderKey() {
|
||||
return self::PROVIDER_DISQUS;
|
||||
}
|
||||
|
||||
public function getProviderName() {
|
||||
return 'Disqus';
|
||||
}
|
||||
|
||||
public function isProviderEnabled() {
|
||||
return PhabricatorEnv::getEnvConfig('disqus.auth-enabled');
|
||||
}
|
||||
|
||||
public function isProviderLinkPermanent() {
|
||||
return PhabricatorEnv::getEnvConfig('disqus.auth-permanent');
|
||||
}
|
||||
|
||||
public function isProviderRegistrationEnabled() {
|
||||
return PhabricatorEnv::getEnvConfig('disqus.registration-enabled');
|
||||
}
|
||||
|
||||
public function getClientID() {
|
||||
return PhabricatorEnv::getEnvConfig('disqus.application-id');
|
||||
}
|
||||
|
||||
public function renderGetClientIDHelp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getClientSecret() {
|
||||
return PhabricatorEnv::getEnvConfig('disqus.application-secret');
|
||||
}
|
||||
|
||||
public function renderGetClientSecretHelp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAuthURI() {
|
||||
return 'https://disqus.com/api/oauth/2.0/authorize/';
|
||||
}
|
||||
|
||||
public function getTokenURI() {
|
||||
return 'https://disqus.com/api/oauth/2.0/access_token/';
|
||||
}
|
||||
|
||||
protected function getTokenExpiryKey() {
|
||||
return 'expires_in';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function getTestURIs() {
|
||||
return array(
|
||||
'http://disqus.com',
|
||||
$this->getUserInfoURI(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getUserInfoURI() {
|
||||
return 'https://disqus.com/api/3.0/users/details.json?'.
|
||||
'api_key='.$this->getClientID();
|
||||
}
|
||||
|
||||
public function getMinimumScope() {
|
||||
return 'read';
|
||||
}
|
||||
|
||||
public function setUserData($data) {
|
||||
$data = idx(json_decode($data, true), 'response');
|
||||
$this->validateUserData($data);
|
||||
$this->userData = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function retrieveUserID() {
|
||||
return $this->userData['id'];
|
||||
}
|
||||
|
||||
public function retrieveUserEmail() {
|
||||
return idx($this->userData, 'email');
|
||||
}
|
||||
|
||||
public function retrieveUserAccountName() {
|
||||
return $this->userData['username'];
|
||||
}
|
||||
|
||||
public function retrieveUserProfileImage() {
|
||||
$avatar = idx($this->userData, 'avatar');
|
||||
if ($avatar) {
|
||||
$uri = idx($avatar, 'permalink');
|
||||
if ($uri) {
|
||||
return @file_get_contents($uri);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function retrieveUserAccountURI() {
|
||||
return idx($this->userData, 'profileUrl');
|
||||
}
|
||||
|
||||
public function retrieveUserRealName() {
|
||||
return idx($this->userData, 'name');
|
||||
}
|
||||
|
||||
public function shouldDiagnoseAppLogin() {
|
||||
return true;
|
||||
}
|
||||
}
|
15
src/applications/auth/oauth/provider/disqus/__init__.php
Normal file
15
src/applications/auth/oauth/provider/disqus/__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('PhabricatorOAuthProviderDisqus.php');
|
|
@ -73,6 +73,10 @@ final class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider {
|
|||
return 'https://graph.facebook.com/oauth/access_token';
|
||||
}
|
||||
|
||||
protected function getTokenExpiryKey() {
|
||||
return 'expires';
|
||||
}
|
||||
|
||||
public function getUserInfoURI() {
|
||||
return 'https://graph.facebook.com/me';
|
||||
}
|
||||
|
|
|
@ -64,6 +64,11 @@ final class PhabricatorOAuthProviderGitHub extends PhabricatorOAuthProvider {
|
|||
return 'https://github.com/login/oauth/access_token';
|
||||
}
|
||||
|
||||
protected function getTokenExpiryKey() {
|
||||
// github access tokens do not have time-based expiry
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTestURIs() {
|
||||
return array(
|
||||
'http://github.com',
|
||||
|
|
|
@ -70,6 +70,10 @@ final class PhabricatorOAuthProviderGoogle extends PhabricatorOAuthProvider {
|
|||
return 'https://accounts.google.com/o/oauth2/token';
|
||||
}
|
||||
|
||||
protected function getTokenExpiryKey() {
|
||||
return 'expires_in';
|
||||
}
|
||||
|
||||
public function getUserInfoURI() {
|
||||
return 'https://www.googleapis.com/oauth2/v1/userinfo';
|
||||
}
|
||||
|
|
|
@ -91,6 +91,10 @@ extends PhabricatorOAuthProvider {
|
|||
return $this->getURI('/oauthserver/token/');
|
||||
}
|
||||
|
||||
protected function getTokenExpiryKey() {
|
||||
return 'expires_in';
|
||||
}
|
||||
|
||||
public function getUserInfoURI() {
|
||||
return $this->getURI('/api/user.whoami');
|
||||
}
|
||||
|
|
|
@ -205,10 +205,7 @@ final class PhabricatorUserOAuthSettingsPanelController
|
|||
$userinfo_uri = new PhutilURI($provider->getUserInfoURI());
|
||||
$token = $oauth_info->getToken();
|
||||
try {
|
||||
$userinfo_uri->setQueryParams(
|
||||
array(
|
||||
'access_token' => $token,
|
||||
));
|
||||
$userinfo_uri->setQueryParam('access_token', $token);
|
||||
$user_data = @file_get_contents($userinfo_uri);
|
||||
$provider->setUserData($user_data);
|
||||
$provider->setAccessToken($token);
|
||||
|
|
Loading…
Reference in a new issue