2011-02-21 07:47:56 +01:00
|
|
|
<?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 PhabricatorOAuthLoginController extends PhabricatorAuthController {
|
|
|
|
|
|
|
|
private $provider;
|
|
|
|
private $userID;
|
2011-02-28 04:47:22 +01:00
|
|
|
|
2011-02-21 07:47:56 +01:00
|
|
|
private $accessToken;
|
2011-02-22 19:24:49 +01:00
|
|
|
private $tokenExpires;
|
2011-03-08 04:29:51 +01:00
|
|
|
private $oauthState;
|
2011-02-21 07:47:56 +01:00
|
|
|
|
|
|
|
public function shouldRequireLogin() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function willProcessRequest(array $data) {
|
|
|
|
$this->provider = PhabricatorOAuthProvider::newProvider($data['provider']);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function processRequest() {
|
|
|
|
$current_user = $this->getRequest()->getUser();
|
|
|
|
|
|
|
|
$provider = $this->provider;
|
|
|
|
if (!$provider->isProviderEnabled()) {
|
|
|
|
return new Aphront400Response();
|
|
|
|
}
|
|
|
|
|
2011-02-22 07:51:34 +01:00
|
|
|
$provider_name = $provider->getProviderName();
|
|
|
|
$provider_key = $provider->getProviderKey();
|
|
|
|
|
2011-02-21 07:47:56 +01:00
|
|
|
$request = $this->getRequest();
|
|
|
|
|
|
|
|
if ($request->getStr('error')) {
|
|
|
|
$error_view = id(new PhabricatorOAuthFailureView())
|
|
|
|
->setRequest($request);
|
|
|
|
return $this->buildErrorResponse($error_view);
|
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$error_response = $this->retrieveAccessToken($provider);
|
|
|
|
if ($error_response) {
|
|
|
|
return $error_response;
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$userinfo_uri = new PhutilURI($provider->getUserInfoURI());
|
|
|
|
$userinfo_uri->setQueryParams(
|
|
|
|
array(
|
2011-02-28 04:47:22 +01:00
|
|
|
'access_token' => $this->accessToken,
|
2011-02-21 07:47:56 +01:00
|
|
|
));
|
|
|
|
|
|
|
|
$user_json = @file_get_contents($userinfo_uri);
|
|
|
|
$user_data = json_decode($user_json, true);
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$provider->setUserData($user_data);
|
|
|
|
$provider->setAccessToken($this->accessToken);
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$user_id = $provider->retrieveUserID();
|
|
|
|
$provider_key = $provider->getProviderKey();
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$oauth_info = $this->retrieveOAuthInfo($provider);
|
2011-02-22 07:51:34 +01:00
|
|
|
|
|
|
|
if ($current_user->getPHID()) {
|
2011-02-28 04:47:22 +01:00
|
|
|
if ($oauth_info->getID()) {
|
|
|
|
if ($oauth_info->getUserID() != $current_user->getID()) {
|
2011-02-22 07:51:34 +01:00
|
|
|
$dialog = new AphrontDialogView();
|
|
|
|
$dialog->setUser($current_user);
|
|
|
|
$dialog->setTitle('Already Linked to Another Account');
|
|
|
|
$dialog->appendChild(
|
|
|
|
'<p>The '.$provider_name.' account you just authorized '.
|
|
|
|
'is already linked to another Phabricator account. Before you can '.
|
|
|
|
'associate your '.$provider_name.' account with this Phabriactor '.
|
|
|
|
'account, you must unlink it from the Phabricator account it is '.
|
|
|
|
'currently linked to.</p>');
|
|
|
|
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
|
|
|
|
|
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
|
|
|
} else {
|
|
|
|
return id(new AphrontRedirectResponse())
|
|
|
|
->setURI('/settings/page/'.$provider_key.'/');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$existing_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
|
|
|
|
'userID = %d AND oauthProvider = %s',
|
|
|
|
$current_user->getID(),
|
|
|
|
$provider_key);
|
|
|
|
|
|
|
|
if ($existing_oauth) {
|
|
|
|
$dialog = new AphrontDialogView();
|
|
|
|
$dialog->setUser($current_user);
|
|
|
|
$dialog->setTitle('Already Linked to an Account From This Provider');
|
|
|
|
$dialog->appendChild(
|
|
|
|
'<p>The account you are logged in with is already linked to a '.
|
|
|
|
$provider_name.' account. Before you can link it to a different '.
|
|
|
|
$provider_name.' account, you must unlink the old account.</p>');
|
|
|
|
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
|
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
|
|
|
}
|
|
|
|
|
2011-02-22 07:51:34 +01:00
|
|
|
if (!$request->isDialogFormPost()) {
|
|
|
|
$dialog = new AphrontDialogView();
|
|
|
|
$dialog->setUser($current_user);
|
|
|
|
$dialog->setTitle('Link '.$provider_name.' Account');
|
|
|
|
$dialog->appendChild(
|
|
|
|
'<p>Link your '.$provider_name.' account to your Phabricator '.
|
|
|
|
'account?</p>');
|
2011-02-28 04:47:22 +01:00
|
|
|
$dialog->addHiddenInput('token', $provider->getAccessToken());
|
|
|
|
$dialog->addHiddenInput('expires', $oauth_info->getTokenExpires());
|
2011-03-08 04:29:51 +01:00
|
|
|
$dialog->addHiddenInput('state', $this->oauthState);
|
2011-02-22 07:51:34 +01:00
|
|
|
$dialog->addSubmitButton('Link Accounts');
|
|
|
|
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
|
|
|
|
|
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
|
|
|
}
|
|
|
|
|
|
|
|
$oauth_info->setUserID($current_user->getID());
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
|
|
|
|
$this->saveOAuthInfo($oauth_info);
|
2011-02-22 07:51:34 +01:00
|
|
|
|
|
|
|
return id(new AphrontRedirectResponse())
|
|
|
|
->setURI('/settings/page/'.$provider_key.'/');
|
|
|
|
}
|
|
|
|
|
2011-07-07 05:05:35 +02:00
|
|
|
$next_uri = $request->getCookie('next_uri', '/');
|
2011-02-22 07:51:34 +01:00
|
|
|
|
|
|
|
// Login with known auth.
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
if ($oauth_info->getID()) {
|
2011-08-20 00:22:30 +02:00
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$known_user = id(new PhabricatorUser())->load($oauth_info->getUserID());
|
2011-02-28 19:15:42 +01:00
|
|
|
|
|
|
|
$request->getApplicationConfiguration()->willAuthenticateUserWithOAuth(
|
|
|
|
$known_user,
|
|
|
|
$oauth_info,
|
|
|
|
$provider);
|
|
|
|
|
2011-02-21 07:47:56 +01:00
|
|
|
$session_key = $known_user->establishSession('web');
|
2011-02-22 19:24:49 +01:00
|
|
|
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
$this->saveOAuthInfo($oauth_info);
|
2011-02-22 19:24:49 +01:00
|
|
|
|
2011-02-21 07:47:56 +01:00
|
|
|
$request->setCookie('phusr', $known_user->getUsername());
|
|
|
|
$request->setCookie('phsid', $session_key);
|
2011-07-07 05:05:35 +02:00
|
|
|
$request->clearCookie('next_uri');
|
2011-02-21 07:47:56 +01:00
|
|
|
return id(new AphrontRedirectResponse())
|
2011-03-08 04:29:51 +01:00
|
|
|
->setURI($next_uri);
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$oauth_email = $provider->retrieveUserEmail();
|
2011-02-21 07:47:56 +01:00
|
|
|
if ($oauth_email) {
|
|
|
|
$known_email = id(new PhabricatorUser())
|
|
|
|
->loadOneWhere('email = %s', $oauth_email);
|
|
|
|
if ($known_email) {
|
2011-02-22 19:24:49 +01:00
|
|
|
$dialog = new AphrontDialogView();
|
|
|
|
$dialog->setUser($current_user);
|
|
|
|
$dialog->setTitle('Already Linked to Another Account');
|
|
|
|
$dialog->appendChild(
|
|
|
|
'<p>The '.$provider_name.' account you just authorized has an '.
|
|
|
|
'email address which is already in use by another Phabricator '.
|
|
|
|
'account. To link the accounts, log in to your Phabricator '.
|
|
|
|
'account and then go to Settings.</p>');
|
|
|
|
$dialog->addCancelButton('/login/');
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-22 19:24:49 +01:00
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
if (!$provider->isProviderRegistrationEnabled()) {
|
|
|
|
$dialog = new AphrontDialogView();
|
|
|
|
$dialog->setUser($current_user);
|
|
|
|
$dialog->setTitle('No Account Registration With '.$provider_name);
|
|
|
|
$dialog->appendChild(
|
|
|
|
'<p>You can not register a new account using '.$provider_name.'; '.
|
|
|
|
'you can only use your '.$provider_name.' account to log into an '.
|
|
|
|
'existing Phabricator account which you have registered through '.
|
|
|
|
'other means.</p>');
|
|
|
|
$dialog->addCancelButton('/login/');
|
|
|
|
|
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$class = PhabricatorEnv::getEnvConfig('controller.oauth-registration');
|
|
|
|
PhutilSymbolLoader::loadClass($class);
|
|
|
|
$controller = newv($class, array($this->getRequest()));
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$controller->setOAuthProvider($provider);
|
|
|
|
$controller->setOAuthInfo($oauth_info);
|
2011-03-08 04:29:51 +01:00
|
|
|
$controller->setOAuthState($this->oauthState);
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
return $this->delegateToController($controller);
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
private function retrieveAccessToken(PhabricatorOAuthProvider $provider) {
|
|
|
|
$request = $this->getRequest();
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$token = $request->getStr('token');
|
|
|
|
if ($token) {
|
|
|
|
$this->tokenExpires = $request->getInt('expires');
|
|
|
|
$this->accessToken = $token;
|
2011-03-08 04:29:51 +01:00
|
|
|
$this->oauthState = $request->getStr('state');
|
2011-02-28 04:47:22 +01:00
|
|
|
return null;
|
|
|
|
}
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$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,
|
|
|
|
);
|
|
|
|
|
|
|
|
$post_data = http_build_query($query_data);
|
|
|
|
$post_length = strlen($post_data);
|
|
|
|
|
|
|
|
$stream_context = stream_context_create(
|
|
|
|
array(
|
|
|
|
'http' => array(
|
|
|
|
'method' => 'POST',
|
|
|
|
'header' =>
|
|
|
|
"Content-Type: application/x-www-form-urlencoded\r\n".
|
|
|
|
"Content-Length: {$post_length}\r\n",
|
|
|
|
'content' => $post_data,
|
|
|
|
),
|
|
|
|
));
|
|
|
|
|
|
|
|
$stream = fopen($auth_uri, 'r', false, $stream_context);
|
|
|
|
|
|
|
|
$response = false;
|
|
|
|
$meta = null;
|
|
|
|
if ($stream) {
|
|
|
|
$meta = stream_get_meta_data($stream);
|
|
|
|
$response = stream_get_contents($stream);
|
|
|
|
fclose($stream);
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
if ($response === false) {
|
|
|
|
return $this->buildErrorResponse(new PhabricatorOAuthFailureView());
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$data = array();
|
|
|
|
parse_str($response, $data);
|
|
|
|
|
|
|
|
$token = idx($data, 'access_token');
|
|
|
|
if (!$token) {
|
|
|
|
return $this->buildErrorResponse(new PhabricatorOAuthFailureView());
|
2011-02-22 19:24:49 +01:00
|
|
|
}
|
2011-02-28 04:47:22 +01:00
|
|
|
|
|
|
|
if (idx($data, 'expires')) {
|
|
|
|
$this->tokenExpires = time() + $data['expires'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->accessToken = $token;
|
2011-03-08 04:29:51 +01:00
|
|
|
$this->oauthState = $request->getStr('state');
|
2011-02-28 04:47:22 +01:00
|
|
|
|
2011-02-22 19:24:49 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
private function retrieveOAuthInfo(PhabricatorOAuthProvider $provider) {
|
2011-02-21 07:47:56 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
|
|
|
|
'oauthProvider = %s and oauthUID = %s',
|
|
|
|
$provider->getProviderKey(),
|
|
|
|
$provider->retrieveUserID());
|
2011-02-22 19:24:49 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
if (!$oauth_info) {
|
|
|
|
$oauth_info = new PhabricatorUserOAuthInfo();
|
|
|
|
$oauth_info->setOAuthProvider($provider->getProviderKey());
|
|
|
|
$oauth_info->setOAuthUID($provider->retrieveUserID());
|
|
|
|
}
|
2011-02-22 19:24:49 +01:00
|
|
|
|
2011-02-28 04:47:22 +01:00
|
|
|
$oauth_info->setAccountURI($provider->retrieveUserAccountURI());
|
|
|
|
$oauth_info->setAccountName($provider->retrieveUserAccountName());
|
|
|
|
$oauth_info->setToken($provider->getAccessToken());
|
2011-02-22 19:24:49 +01:00
|
|
|
$oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD);
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
$expires = $oauth_info->getTokenExpires();
|
|
|
|
if ($expires <= time()) {
|
|
|
|
$expires = null;
|
|
|
|
}
|
|
|
|
if ($this->tokenExpires) {
|
|
|
|
$expires = $this->tokenExpires;
|
|
|
|
}
|
|
|
|
$oauth_info->setTokenExpires($expires);
|
2011-02-28 04:47:22 +01:00
|
|
|
|
|
|
|
return $oauth_info;
|
2011-02-22 19:24:49 +01:00
|
|
|
}
|
|
|
|
|
Create AphrontWriteGuard, a backup mechanism for CSRF validation
Summary:
Provide a catchall mechanism to find unprotected writes.
- Depends on D758.
- Similar to WriteOnHTTPGet stuff from Facebook's stack.
- Since we have a small number of storage mechanisms and highly structured
read/write pathways, we can explicitly answer the question "is this page
performing a write?".
- Never allow writes without CSRF checks.
- This will probably break some things. That's fine: they're CSRF
vulnerabilities or weird edge cases that we can fix. But don't push to Facebook
for a few days unless you're prepared to deal with this.
- **>>> MEGADERP: All Conduit write APIs are currently vulnerable to CSRF!
<<<**
Test Plan:
- Ran some scripts that perform writes (scripts/search indexers), no issues.
- Performed normal CSRF submits.
- Added writes to an un-CSRF'd page, got an exception.
- Executed conduit methods.
- Did login/logout (this works because the logged-out user validates the
logged-out csrf "token").
- Did OAuth login.
- Did OAuth registration.
Reviewers: pedram, andrewjcg, erling, jungejason, tuomaspelkonen, aran,
codeblock
Commenters: pedram
CC: aran, epriestley, pedram
Differential Revision: 777
2011-08-03 20:49:27 +02:00
|
|
|
private function saveOAuthInfo(PhabricatorUserOAuthInfo $info) {
|
|
|
|
// UNGUARDED WRITES: Logging-in users don't have their CSRF set up yet.
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$info->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2011-02-21 07:47:56 +01:00
|
|
|
}
|