1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-29 02:02:41 +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:
Bob Trahan 2012-05-08 12:08:05 -07:00
parent eb9645e9b4
commit 679f778235
11 changed files with 242 additions and 25 deletions

View file

@ -146,6 +146,9 @@ return array(
'phabricator.csrf-key', 'phabricator.csrf-key',
'facebook.application-secret', 'facebook.application-secret',
'github.application-secret', 'github.application-secret',
'google.application-secret',
'phabricator.application-secret',
'disqus.application-secret',
'phabricator.mail-key', 'phabricator.mail-key',
'security.hmac-key', 'security.hmac-key',
), ),
@ -512,6 +515,25 @@ return array(
// The Google "Client Secret" to use for Google API access. // The Google "Client Secret" to use for Google API access.
'google.application-secret' => null, '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 ----------------------------------------------------- // // -- Phabricator OAuth ----------------------------------------------------- //
// Meta-town -- Phabricator is itself an OAuth Provider // Meta-town -- Phabricator is itself an OAuth Provider

View file

@ -731,6 +731,7 @@ phutil_register_library_map(array(
'PhabricatorOAuthFailureView' => 'applications/auth/view/oauthfailure', 'PhabricatorOAuthFailureView' => 'applications/auth/view/oauthfailure',
'PhabricatorOAuthLoginController' => 'applications/auth/controller/oauth', 'PhabricatorOAuthLoginController' => 'applications/auth/controller/oauth',
'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/base', 'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/base',
'PhabricatorOAuthProviderDisqus' => 'applications/auth/oauth/provider/disqus',
'PhabricatorOAuthProviderException' => 'applications/auth/oauth/provider/exception', 'PhabricatorOAuthProviderException' => 'applications/auth/oauth/provider/exception',
'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/facebook', 'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/facebook',
'PhabricatorOAuthProviderGitHub' => 'applications/auth/oauth/provider/github', 'PhabricatorOAuthProviderGitHub' => 'applications/auth/oauth/provider/github',
@ -1646,6 +1647,7 @@ phutil_register_library_map(array(
'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController',
'PhabricatorOAuthFailureView' => 'AphrontView', 'PhabricatorOAuthFailureView' => 'AphrontView',
'PhabricatorOAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorOAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorOAuthProviderDisqus' => 'PhabricatorOAuthProvider',
'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider',
'PhabricatorOAuthProviderGitHub' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderGitHub' => 'PhabricatorOAuthProvider',
'PhabricatorOAuthProviderGoogle' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderGoogle' => 'PhabricatorOAuthProvider',

View file

@ -59,10 +59,7 @@ final class PhabricatorOAuthLoginController
} }
$userinfo_uri = new PhutilURI($provider->getUserInfoURI()); $userinfo_uri = new PhutilURI($provider->getUserInfoURI());
$userinfo_uri->setQueryParams( $userinfo_uri->setQueryParam('access_token', $this->accessToken);
array(
'access_token' => $this->accessToken,
));
try { try {
$user_data = @file_get_contents($userinfo_uri); $user_data = @file_get_contents($userinfo_uri);
@ -129,9 +126,10 @@ final class PhabricatorOAuthLoginController
hsprintf( hsprintf(
'<p>Link your %s account to your Phabricator account?</p>', '<p>Link your %s account to your Phabricator account?</p>',
$provider_name)); $provider_name));
$dialog->addHiddenInput('token', $provider->getAccessToken()); $dialog->addHiddenInput('confirm_token', $provider->getAccessToken());
$dialog->addHiddenInput('expires', $oauth_info->getTokenExpires()); $dialog->addHiddenInput('expires', $oauth_info->getTokenExpires());
$dialog->addHiddenInput('state', $this->oauthState); $dialog->addHiddenInput('state', $this->oauthState);
$dialog->addHiddenInput('scope', $oauth_info->getTokenScope());
$dialog->addSubmitButton('Link Accounts'); $dialog->addSubmitButton('Link Accounts');
$dialog->addCancelButton('/settings/page/'.$provider_key.'/'); $dialog->addCancelButton('/settings/page/'.$provider_key.'/');
@ -238,18 +236,18 @@ final class PhabricatorOAuthLoginController
private function retrieveAccessToken(PhabricatorOAuthProvider $provider) { private function retrieveAccessToken(PhabricatorOAuthProvider $provider) {
$request = $this->getRequest(); $request = $this->getRequest();
$token = $request->getStr('token'); $token = $request->getStr('confirm_token');
if ($token) { if ($token) {
$this->tokenExpires = $request->getInt('expires'); $this->tokenExpires = $request->getInt('expires');
$this->accessToken = $token; $this->accessToken = $token;
$this->oauthState = $request->getStr('state'); $this->oauthState = $request->getStr('state');
return null; return null;
} }
$client_id = $provider->getClientID(); $client_id = $provider->getClientID();
$client_secret = $provider->getClientSecret(); $client_secret = $provider->getClientSecret();
$redirect_uri = $provider->getRedirectURI(); $redirect_uri = $provider->getRedirectURI();
$auth_uri = $provider->getTokenURI(); $auth_uri = $provider->getTokenURI();
$code = $request->getStr('code'); $code = $request->getStr('code');
$query_data = array( $query_data = array(
@ -294,12 +292,9 @@ final class PhabricatorOAuthLoginController
return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); return $this->buildErrorResponse(new PhabricatorOAuthFailureView());
} }
if (idx($data, 'expires')) { $this->tokenExpires = $provider->getTokenExpiryFromArray($data);
$this->tokenExpires = time() + $data['expires']; $this->accessToken = $token;
} $this->oauthState = $request->getStr('state');
$this->accessToken = $token;
$this->oauthState = $request->getStr('state');
return null; return null;
} }
@ -311,16 +306,24 @@ final class PhabricatorOAuthLoginController
$provider->getProviderKey(), $provider->getProviderKey(),
$provider->retrieveUserID()); $provider->retrieveUserID());
$scope = $this->getRequest()->getStr('scope');
if (!$oauth_info) { if (!$oauth_info) {
$oauth_info = new PhabricatorUserOAuthInfo(); $oauth_info = new PhabricatorUserOAuthInfo();
$oauth_info->setOAuthProvider($provider->getProviderKey()); $oauth_info->setOAuthProvider($provider->getProviderKey());
$oauth_info->setOAuthUID($provider->retrieveUserID()); $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->setAccountURI($provider->retrieveUserAccountURI());
$oauth_info->setAccountName($provider->retrieveUserAccountName()); $oauth_info->setAccountName($provider->retrieveUserAccountName());
$oauth_info->setToken($provider->getAccessToken()); $oauth_info->setToken($provider->getAccessToken());
$oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD); $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 // 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. // it with good info if the provider gave it to us.
@ -341,7 +344,4 @@ final class PhabricatorOAuthLoginController
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$info->save(); $info->save();
} }
} }

View file

@ -22,6 +22,7 @@ abstract class PhabricatorOAuthProvider {
const PROVIDER_GITHUB = 'github'; const PROVIDER_GITHUB = 'github';
const PROVIDER_GOOGLE = 'google'; const PROVIDER_GOOGLE = 'google';
const PROVIDER_PHABRICATOR = 'phabricator'; const PROVIDER_PHABRICATOR = 'phabricator';
const PROVIDER_DISQUS = 'disqus';
private $accessToken; private $accessToken;
@ -55,6 +56,21 @@ abstract class PhabricatorOAuthProvider {
abstract public function getTokenURI(); 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. * If the provider needs extra stuff in the token request, return it here.
* For example, Google needs a grant_type parameter. * For example, Google needs a grant_type parameter.
@ -133,6 +149,9 @@ abstract class PhabricatorOAuthProvider {
case self::PROVIDER_PHABRICATOR: case self::PROVIDER_PHABRICATOR:
$class = 'PhabricatorOAuthProviderPhabricator'; $class = 'PhabricatorOAuthProviderPhabricator';
break; break;
case self::PROVIDER_DISQUS:
$class = 'PhabricatorOAuthProviderDisqus';
break;
default: default:
throw new Exception('Unknown OAuth provider.'); throw new Exception('Unknown OAuth provider.');
} }
@ -146,6 +165,7 @@ abstract class PhabricatorOAuthProvider {
self::PROVIDER_GITHUB, self::PROVIDER_GITHUB,
self::PROVIDER_GOOGLE, self::PROVIDER_GOOGLE,
self::PROVIDER_PHABRICATOR, self::PROVIDER_PHABRICATOR,
self::PROVIDER_DISQUS,
); );
$providers = array(); $providers = array();
foreach ($all as $provider) { foreach ($all as $provider) {

View file

@ -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;
}
}

View 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');

View file

@ -73,6 +73,10 @@ final class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider {
return 'https://graph.facebook.com/oauth/access_token'; return 'https://graph.facebook.com/oauth/access_token';
} }
protected function getTokenExpiryKey() {
return 'expires';
}
public function getUserInfoURI() { public function getUserInfoURI() {
return 'https://graph.facebook.com/me'; return 'https://graph.facebook.com/me';
} }

View file

@ -64,6 +64,11 @@ final class PhabricatorOAuthProviderGitHub extends PhabricatorOAuthProvider {
return 'https://github.com/login/oauth/access_token'; 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() { public function getTestURIs() {
return array( return array(
'http://github.com', 'http://github.com',

View file

@ -70,6 +70,10 @@ final class PhabricatorOAuthProviderGoogle extends PhabricatorOAuthProvider {
return 'https://accounts.google.com/o/oauth2/token'; return 'https://accounts.google.com/o/oauth2/token';
} }
protected function getTokenExpiryKey() {
return 'expires_in';
}
public function getUserInfoURI() { public function getUserInfoURI() {
return 'https://www.googleapis.com/oauth2/v1/userinfo'; return 'https://www.googleapis.com/oauth2/v1/userinfo';
} }

View file

@ -91,6 +91,10 @@ extends PhabricatorOAuthProvider {
return $this->getURI('/oauthserver/token/'); return $this->getURI('/oauthserver/token/');
} }
protected function getTokenExpiryKey() {
return 'expires_in';
}
public function getUserInfoURI() { public function getUserInfoURI() {
return $this->getURI('/api/user.whoami'); return $this->getURI('/api/user.whoami');
} }

View file

@ -205,10 +205,7 @@ final class PhabricatorUserOAuthSettingsPanelController
$userinfo_uri = new PhutilURI($provider->getUserInfoURI()); $userinfo_uri = new PhutilURI($provider->getUserInfoURI());
$token = $oauth_info->getToken(); $token = $oauth_info->getToken();
try { try {
$userinfo_uri->setQueryParams( $userinfo_uri->setQueryParam('access_token', $token);
array(
'access_token' => $token,
));
$user_data = @file_get_contents($userinfo_uri); $user_data = @file_get_contents($userinfo_uri);
$provider->setUserData($user_data); $provider->setUserData($user_data);
$provider->setAccessToken($token); $provider->setAccessToken($token);