1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-09-22 18:28:47 +02:00
phorge-phorge/src/applications/auth/controller/PhabricatorAuthLoginController.php

253 lines
7.9 KiB
PHP
Raw Normal View History

<?php
final class PhabricatorAuthLoginController
extends PhabricatorAuthController {
private $providerKey;
private $extraURIData;
private $provider;
public function shouldRequireLogin() {
return false;
}
Whitelist controllers which can receive a 'code' parameter Summary: Ref T4593. There are a variety of clever attacks against OAuth which involve changing the redirect URI to some other URI on the same domain which exhibits unexpected behavior in response to an OAuth request. The best approach to dealing with this is for providers to lock to a specific path and refuse to redirect elsewhere, but not all providers do this. We haven't had any specific issues related to this, but the anchor issue in T4593 was only a step away. To mitigate this in general, we can reject the OAuth2 `'code'` parameter on //every// page by default, and then whitelist it on the tiny number of controllers which should be able to receive it. This is very coarse, kind of overkill, and has some fallout (we can't use `'code'` as a normal parameter in the application), but I think it's relatively well-contained and seems reasonable. A better approach might be to whitelist parameters on every controller (i.e., have each controller specify the parameters it can receive), but that would be a ton of work and probably cause a lot of false positives for a long time. Since we don't use `'code'` normally anywhere (as far as I can tell), the coarseness of this approach seems reasonable. Test Plan: - Logged in with OAuth. - Hit any other page with `?code=...` in the URL, got an exception. - Grepped for `'code'` and `"code"`, and examined each use to see if it was impacted. Reviewers: btrahan Reviewed By: btrahan Subscribers: aran, epriestley Maniphest Tasks: T4593 Differential Revision: https://secure.phabricator.com/D8499
2014-03-12 19:30:04 +01:00
public function shouldAllowRestrictedParameter($parameter_name) {
// Whitelist the OAuth 'code' parameter.
if ($parameter_name == 'code') {
return true;
}
return parent::shouldAllowRestrictedParameter($parameter_name);
}
public function willProcessRequest(array $data) {
$this->providerKey = $data['pkey'];
$this->extraURIData = idx($data, 'extra');
}
public function getExtraURIData() {
return $this->extraURIData;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$response = $this->loadProvider();
if ($response) {
return $response;
}
$provider = $this->provider;
try {
list($account, $response) = $provider->processLoginRequest($this);
} catch (PhutilAuthUserAbortedException $ex) {
if ($viewer->isLoggedIn()) {
// If a logged-in user cancels, take them back to the external accounts
// panel.
$next_uri = '/settings/panel/external/';
} else {
// If a logged-out user cancels, take them back to the auth start page.
$next_uri = '/';
}
// User explicitly hit "Cancel".
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Authentication Canceled'))
->appendChild(
pht('You canceled authentication.'))
->addCancelButton($next_uri, pht('Continue'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($response) {
return $response;
}
Add password authentication and registration to new registration Summary: Ref T1536. Ref T1930. Code is not reachable. This provides password authentication and registration on the new provider/adapter framework. I sort of cheated a little bit and don't really route any password logic through the adapter (instead, this provider uses an empty adapter and just sets the type/domain on it). I think the right way to do this //conceptually// is to treat username/passwords as an external black box which the adapter communicates with. However, this creates a lot of practical implementation and UX problems: - There would basically be two steps -- in the first one, you interact with the "password black box", which behaves like an OAuth provider. This produces some ExternalAccount associated with the username/password pair, then we go into normal registration. - In normal registration, we'd proceed normally. This means: - The registration flow would be split into two parts, one where you select a username/password (interacting with the black box) and one where you actually register (interacting with the generic flow). This is unusual and probably confusing for users. - We would need to do a lot of re-hashing of passwords, since passwords currently depend on the username and user PHID, which won't exist yet during registration or the "black box" phase. This is a big mess I don't want to deal with. - We hit a weird condition where two users complete step 1 with the same username but don't complete step 2 yet. The box knows about two different copies of the username, with two different passwords. When we arrive at step 2 the second time we have a lot of bad choices about how to reoslve it, most of which create security problems. The most stragihtforward and "pure" way to resolve the issues is to put password-auth usernames in a separate space, but this would be incredibly confusuing to users (your login name might not be the same as your username, which is bizarre). - If we change this, we need to update all the other password-related code, which I don't want to bother with (at least for now). Instead, let registration know about a "default" registration controller (which is always password, if enabled), and let it require a password. This gives us a much simpler (albeit slightly less pure) implementation: - All the fields are on one form. - Password adapter is just a shell. - Password provider does the heavy lifting. We might make this more pure at some point, but I'm generally pretty satisfied with this. This doesn't implement the brute-force CAPTCHA protection, that will be coming soon. Test Plan: Registered with password only and logged in with a password. Hit various error conditions. Reviewers: btrahan Reviewed By: btrahan CC: aran, chad Maniphest Tasks: T1536, T1930 Differential Revision: https://secure.phabricator.com/D6164
2013-06-16 19:15:49 +02:00
if (!$account) {
throw new Exception(
"Auth provider failed to load an account from processLoginRequest()!");
}
if ($account->getUserPHID()) {
// The account is already attached to a Phabricator user, so this is
// either a login or a bad account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowLogin()) {
return $this->processLoginUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow logins on this Phabricator install. '.
'An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else if ($viewer->getPHID() == $account->getUserPHID()) {
// This is either an attempt to re-link an existing and already
// linked account (which is silly) or a refresh of an external account
// (e.g., an OAuth account).
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/external/');
} else {
return $this->renderError(
pht(
'The external account ("%s") you just used to login is alerady '.
'associated with another Phabricator user account. Login to the '.
'other Phabricator account and unlink the external account before '.
'linking it to a new Phabricator account.',
$provider->getProviderName()));
}
} else {
// The account is not yet attached to a Phabricator user, so this is
// either a registration or an account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowRegistration()) {
return $this->processRegisterUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow registration on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else {
if ($provider->shouldAllowAccountLink()) {
return $this->processLinkUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow account linking on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
}
}
// This should be unreachable, but fail explicitly if we get here somehow.
return new Aphront400Response();
}
private function processLoginUser(PhabricatorExternalAccount $account) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$account->getUserPHID());
if (!$user) {
return $this->renderError(
pht(
'The external account you just logged in with is not associated '.
'with a valid Phabricator user.'));
}
return $this->loginUser($user);
}
private function processRegisterUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $register_uri);
}
private function processLinkUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $confirm_uri);
}
private function setAccountKeyAndContinue(
PhabricatorExternalAccount $account,
$next_uri) {
if ($account->getUserPHID()) {
throw new Exception("Account is already registered or linked.");
}
// Regenerate the registration secret key, set it on the external account,
// set a cookie on the user's machine, and redirect them to registration.
// See PhabricatorAuthRegisterController for discussion of the registration
// key.
$registration_key = Filesystem::readRandomCharacters(32);
$account->setProperty(
'registrationKey',
PhabricatorHash::digest($registration_key));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
$this->getRequest()->setTemporaryCookie(
PhabricatorCookies::COOKIE_REGISTRATION,
$registration_key);
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
private function loadProvider() {
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->providerKey);
if (!$provider) {
return $this->renderError(
pht(
'The account you are attempting to login with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$this->providerKey));
}
$this->provider = $provider;
return null;
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Login Failed'),
array($message));
}
Add password authentication and registration to new registration Summary: Ref T1536. Ref T1930. Code is not reachable. This provides password authentication and registration on the new provider/adapter framework. I sort of cheated a little bit and don't really route any password logic through the adapter (instead, this provider uses an empty adapter and just sets the type/domain on it). I think the right way to do this //conceptually// is to treat username/passwords as an external black box which the adapter communicates with. However, this creates a lot of practical implementation and UX problems: - There would basically be two steps -- in the first one, you interact with the "password black box", which behaves like an OAuth provider. This produces some ExternalAccount associated with the username/password pair, then we go into normal registration. - In normal registration, we'd proceed normally. This means: - The registration flow would be split into two parts, one where you select a username/password (interacting with the black box) and one where you actually register (interacting with the generic flow). This is unusual and probably confusing for users. - We would need to do a lot of re-hashing of passwords, since passwords currently depend on the username and user PHID, which won't exist yet during registration or the "black box" phase. This is a big mess I don't want to deal with. - We hit a weird condition where two users complete step 1 with the same username but don't complete step 2 yet. The box knows about two different copies of the username, with two different passwords. When we arrive at step 2 the second time we have a lot of bad choices about how to reoslve it, most of which create security problems. The most stragihtforward and "pure" way to resolve the issues is to put password-auth usernames in a separate space, but this would be incredibly confusuing to users (your login name might not be the same as your username, which is bizarre). - If we change this, we need to update all the other password-related code, which I don't want to bother with (at least for now). Instead, let registration know about a "default" registration controller (which is always password, if enabled), and let it require a password. This gives us a much simpler (albeit slightly less pure) implementation: - All the fields are on one form. - Password adapter is just a shell. - Password provider does the heavy lifting. We might make this more pure at some point, but I'm generally pretty satisfied with this. This doesn't implement the brute-force CAPTCHA protection, that will be coming soon. Test Plan: Registered with password only and logged in with a password. Hit various error conditions. Reviewers: btrahan Reviewed By: btrahan CC: aran, chad Maniphest Tasks: T1536, T1930 Differential Revision: https://secure.phabricator.com/D6164
2013-06-16 19:15:49 +02:00
public function buildProviderPageResponse(
PhabricatorAuthProvider $provider,
$content) {
$crumbs = $this->buildApplicationCrumbs();
if ($this->getRequest()->getUser()->isLoggedIn()) {
$crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI());
} else {
$crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/'));
}
$crumbs->addTextCrumb($provider->getProviderName());
Add password authentication and registration to new registration Summary: Ref T1536. Ref T1930. Code is not reachable. This provides password authentication and registration on the new provider/adapter framework. I sort of cheated a little bit and don't really route any password logic through the adapter (instead, this provider uses an empty adapter and just sets the type/domain on it). I think the right way to do this //conceptually// is to treat username/passwords as an external black box which the adapter communicates with. However, this creates a lot of practical implementation and UX problems: - There would basically be two steps -- in the first one, you interact with the "password black box", which behaves like an OAuth provider. This produces some ExternalAccount associated with the username/password pair, then we go into normal registration. - In normal registration, we'd proceed normally. This means: - The registration flow would be split into two parts, one where you select a username/password (interacting with the black box) and one where you actually register (interacting with the generic flow). This is unusual and probably confusing for users. - We would need to do a lot of re-hashing of passwords, since passwords currently depend on the username and user PHID, which won't exist yet during registration or the "black box" phase. This is a big mess I don't want to deal with. - We hit a weird condition where two users complete step 1 with the same username but don't complete step 2 yet. The box knows about two different copies of the username, with two different passwords. When we arrive at step 2 the second time we have a lot of bad choices about how to reoslve it, most of which create security problems. The most stragihtforward and "pure" way to resolve the issues is to put password-auth usernames in a separate space, but this would be incredibly confusuing to users (your login name might not be the same as your username, which is bizarre). - If we change this, we need to update all the other password-related code, which I don't want to bother with (at least for now). Instead, let registration know about a "default" registration controller (which is always password, if enabled), and let it require a password. This gives us a much simpler (albeit slightly less pure) implementation: - All the fields are on one form. - Password adapter is just a shell. - Password provider does the heavy lifting. We might make this more pure at some point, but I'm generally pretty satisfied with this. This doesn't implement the brute-force CAPTCHA protection, that will be coming soon. Test Plan: Registered with password only and logged in with a password. Hit various error conditions. Reviewers: btrahan Reviewed By: btrahan CC: aran, chad Maniphest Tasks: T1536, T1930 Differential Revision: https://secure.phabricator.com/D6164
2013-06-16 19:15:49 +02:00
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => pht('Login'),
'device' => true,
));
}
public function buildProviderErrorResponse(
PhabricatorAuthProvider $provider,
$message) {
$message = pht(
'Authentication provider ("%s") encountered an error during login. %s',
$provider->getProviderName(),
$message);
return $this->renderError($message);
}
}