Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php
index 2573bf3ec6..9675d21d42 100644
--- a/src/applications/auth/constants/PhabricatorCookies.php
+++ b/src/applications/auth/constants/PhabricatorCookies.php
@@ -1,179 +1,179 @@
<?php
/**
* Consolidates Phabricator application cookies, including registration
* and session management.
*
* @task clientid Client ID Cookie
* @task next Next URI Cookie
*/
final class PhabricatorCookies extends Phobject {
/**
* Stores the login username for password authentication. This is just a
* display value for convenience, used to prefill the login form. It is not
* authoritative.
*/
const COOKIE_USERNAME = 'phusr';
/**
* Stores the user's current session ID. This is authoritative and establishes
* the user's identity.
*/
const COOKIE_SESSION = 'phsid';
/**
* Stores a secret used during new account registration to prevent an attacker
* from tricking a victim into registering an account which is linked to
* credentials the attacker controls.
*/
const COOKIE_REGISTRATION = 'phreg';
/**
* Stores a secret used during OAuth2 handshakes to prevent various attacks
* where an attacker hands a victim a URI corresponding to the middle of an
* OAuth2 workflow and we might otherwise do something sketchy. Particularly,
* this corresponds to the OAuth2 "code".
*/
const COOKIE_CLIENTID = 'phcid';
/**
* Stores the URI to redirect the user to after login. This allows users to
* visit a path like `/feed/`, be prompted to login, and then be redirected
* back to `/feed/` after the workflow completes.
*/
const COOKIE_NEXTURI = 'next_uri';
/**
* Stores a hint that the user should be moved directly into high security
* after upgrading a partial login session. This is used during password
* recovery to avoid a double-prompt.
*/
const COOKIE_HISEC = 'jump_to_hisec';
/**
* Stores an invite code.
*/
const COOKIE_INVITE = 'invite';
/**
* Stores a workflow completion across a redirect-after-POST following a
* form submission. This can be used to show "Changes Saved" messages.
*/
const COOKIE_SUBMIT = 'phfrm';
/* -( Client ID Cookie )--------------------------------------------------- */
/**
* Set the client ID cookie. This is a random cookie used like a CSRF value
* during authentication workflows.
*
* @param AphrontRequest Request to modify.
* @return void
* @task clientid
*/
public static function setClientIDCookie(AphrontRequest $request) {
// NOTE: See T3471 for some discussion. Some browsers and browser extensions
// can make duplicate requests, so we overwrite this cookie only if it is
// not present in the request. The cookie lifetime is limited by making it
// temporary and clearing it when users log out.
$value = $request->getCookie(self::COOKIE_CLIENTID);
- if (!strlen($value)) {
+ if (!phutil_nonempty_string($value)) {
$request->setTemporaryCookie(
self::COOKIE_CLIENTID,
Filesystem::readRandomCharacters(16));
}
}
/* -( Next URI Cookie )---------------------------------------------------- */
/**
* Set the Next URI cookie. We only write the cookie if it wasn't recently
* written, to avoid writing over a real URI with a bunch of "humans.txt"
* stuff. See T3793 for discussion.
*
* @param AphrontRequest Request to write to.
* @param string URI to write.
* @param bool Write this cookie even if we have a fresh
* cookie already.
* @return void
*
* @task next
*/
public static function setNextURICookie(
AphrontRequest $request,
$next_uri,
$force = false) {
if (!$force) {
$cookie_value = $request->getCookie(self::COOKIE_NEXTURI);
list($set_at, $current_uri) = self::parseNextURICookie($cookie_value);
// If the cookie was set within the last 2 minutes, don't overwrite it.
// Primarily, this prevents browser requests for resources which do not
// exist (like "humans.txt" and various icons) from overwriting a normal
// URI like "/feed/".
if ($set_at > (time() - 120)) {
return;
}
}
$new_value = time().','.$next_uri;
$request->setTemporaryCookie(self::COOKIE_NEXTURI, $new_value);
}
/**
* Read the URI out of the Next URI cookie.
*
* @param AphrontRequest Request to examine.
* @return string|null Next URI cookie's URI value.
*
* @task next
*/
public static function getNextURICookie(AphrontRequest $request) {
$cookie_value = $request->getCookie(self::COOKIE_NEXTURI);
list($set_at, $next_uri) = self::parseNextURICookie($cookie_value);
return $next_uri;
}
/**
* Parse a Next URI cookie into its components.
*
* @param string Raw cookie value.
* @return list<string> List of timestamp and URI.
*
* @task next
*/
private static function parseNextURICookie($cookie) {
// Old cookies look like: /uri
// New cookies look like: timestamp,/uri
if (!strlen($cookie)) {
return null;
}
if (strpos($cookie, ',') !== false) {
list($timestamp, $uri) = explode(',', $cookie, 2);
return array((int)$timestamp, $uri);
}
return array(0, $cookie);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php
index aee93b98d6..8bba90a1e6 100644
--- a/src/applications/auth/controller/PhabricatorAuthController.php
+++ b/src/applications/auth/controller/PhabricatorAuthController.php
@@ -1,299 +1,299 @@
<?php
abstract class PhabricatorAuthController extends PhabricatorController {
protected function renderErrorPage($title, array $messages) {
$view = new PHUIInfoView();
$view->setTitle($title);
$view->setErrors($messages);
return $this->newPage()
->setTitle($title)
->appendChild($view);
}
/**
* Returns true if this install is newly setup (i.e., there are no user
* accounts yet). In this case, we enter a special mode to permit creation
* of the first account form the web UI.
*/
protected function isFirstTimeSetup() {
// If there are any auth providers, this isn't first time setup, even if
// we don't have accounts.
if (PhabricatorAuthProvider::getAllEnabledProviders()) {
return false;
}
// Otherwise, check if there are any user accounts. If not, we're in first
// time setup.
$any_users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->execute();
return !$any_users;
}
/**
* Log a user into a web session and return an @{class:AphrontResponse} which
* corresponds to continuing the login process.
*
* Normally, this is a redirect to the validation controller which makes sure
* the user's cookies are set. However, event listeners can intercept this
* event and do something else if they prefer.
*
* @param PhabricatorUser User to log the viewer in as.
* @param bool True to issue a full session immediately, bypassing MFA.
* @return AphrontResponse Response which continues the login process.
*/
protected function loginUser(
PhabricatorUser $user,
$force_full_session = false) {
$response = $this->buildLoginValidateResponse($user);
$session_type = PhabricatorAuthSession::TYPE_WEB;
if ($force_full_session) {
$partial_session = false;
} else {
$partial_session = true;
}
$session_key = id(new PhabricatorAuthSessionEngine())
->establishSession($session_type, $user->getPHID(), $partial_session);
// NOTE: We allow disabled users to login and roadblock them later, so
// there's no check for users being disabled here.
$request = $this->getRequest();
$request->setCookie(
PhabricatorCookies::COOKIE_USERNAME,
$user->getUsername());
$request->setCookie(
PhabricatorCookies::COOKIE_SESSION,
$session_key);
$this->clearRegistrationCookies();
return $response;
}
protected function clearRegistrationCookies() {
$request = $this->getRequest();
// Clear the registration key.
$request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION);
// Clear the client ID / OAuth state key.
$request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
// Clear the invite cookie.
$request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
}
private function buildLoginValidateResponse(PhabricatorUser $user) {
$validate_uri = new PhutilURI($this->getApplicationURI('validate/'));
$validate_uri->replaceQueryParam('expect', $user->getUsername());
return id(new AphrontRedirectResponse())->setURI((string)$validate_uri);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Error'),
array(
$message,
));
}
protected function loadAccountForRegistrationOrLinking($account_key) {
$request = $this->getRequest();
$viewer = $request->getUser();
$account = null;
$provider = null;
$response = null;
if (!$account_key) {
$response = $this->renderError(
pht('Request did not include account key.'));
return array($account, $provider, $response);
}
// NOTE: We're using the omnipotent user because the actual user may not
// be logged in yet, and because we want to tailor an error message to
// distinguish between "not usable" and "does not exist". We do explicit
// checks later on to make sure this account is valid for the intended
// operation. This requires edit permission for completeness and consistency
// but it won't actually be meaningfully checked because we're using the
// omnipotent user.
$account = id(new PhabricatorExternalAccountQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAccountSecrets(array($account_key))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$account) {
$response = $this->renderError(pht('No valid linkable account.'));
return array($account, $provider, $response);
}
if ($account->getUserPHID()) {
if ($account->getUserPHID() != $viewer->getPHID()) {
$response = $this->renderError(
pht(
'The account you are attempting to register or link is already '.
'linked to another user.'));
} else {
$response = $this->renderError(
pht(
'The account you are attempting to link is already linked '.
'to your account.'));
}
return array($account, $provider, $response);
}
$registration_key = $request->getCookie(
PhabricatorCookies::COOKIE_REGISTRATION);
// NOTE: This registration key check is not strictly necessary, because
// we're only creating new accounts, not linking existing accounts. It
// might be more hassle than it is worth, especially for email.
//
// The attack this prevents is getting to the registration screen, then
// copy/pasting the URL and getting someone else to click it and complete
// the process. They end up with an account bound to credentials you
// control. This doesn't really let you do anything meaningful, though,
// since you could have simply completed the process yourself.
if (!$registration_key) {
$response = $this->renderError(
pht(
'Your browser did not submit a registration key with the request. '.
'You must use the same browser to begin and complete registration. '.
'Check that cookies are enabled and try again.'));
return array($account, $provider, $response);
}
// We store the digest of the key rather than the key itself to prevent a
// theoretical attacker with read-only access to the database from
// hijacking registration sessions.
$actual = $account->getProperty('registrationKey');
$expect = PhabricatorHash::weakDigest($registration_key);
if (!phutil_hashes_are_identical($actual, $expect)) {
$response = $this->renderError(
pht(
'Your browser submitted a different registration key than the one '.
'associated with this account. You may need to clear your cookies.'));
return array($account, $provider, $response);
}
$config = $account->getProviderConfig();
if (!$config->getIsEnabled()) {
$response = $this->renderError(
pht(
'The account you are attempting to register with uses a disabled '.
'authentication provider ("%s"). An administrator may have '.
'recently disabled this provider.',
$config->getDisplayName()));
return array($account, $provider, $response);
}
$provider = $config->getProvider();
return array($account, $provider, null);
}
protected function loadInvite() {
$invite_cookie = PhabricatorCookies::COOKIE_INVITE;
$invite_code = $this->getRequest()->getCookie($invite_cookie);
if (!$invite_code) {
return null;
}
$engine = id(new PhabricatorAuthInviteEngine())
->setViewer($this->getViewer())
->setUserHasConfirmedVerify(true);
try {
return $engine->processInviteCode($invite_code);
} catch (Exception $ex) {
// If this fails for any reason, just drop the invite. In normal
// circumstances, we gave them a detailed explanation of any error
// before they jumped into this workflow.
return null;
}
}
protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
$viewer = $this->getViewer();
// Since the user hasn't registered yet, they may not be able to see other
// user accounts. Load the inviting user with the omnipotent viewer.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$invite_author = id(new PhabricatorPeopleQuery())
->setViewer($omnipotent_viewer)
->withPHIDs(array($invite->getAuthorPHID()))
->needProfileImage(true)
->executeOne();
// If we can't load the author for some reason, just drop this message.
// We lose the value of contextualizing things without author details.
if (!$invite_author) {
return null;
}
$invite_item = id(new PHUIObjectItemView())
->setHeader(
pht(
'Welcome to %s!',
PlatformSymbols::getPlatformServerName()))
->setImageURI($invite_author->getProfileImageURI())
->addAttribute(
pht(
'%s has invited you to join %s.',
$invite_author->getFullName(),
PlatformSymbols::getPlatformServerName()));
$invite_list = id(new PHUIObjectItemListView())
->addItem($invite_item)
->setFlush(true);
return id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($invite_list);
}
final protected function newCustomStartMessage() {
$viewer = $this->getViewer();
$text = PhabricatorAuthMessage::loadMessageText(
$viewer,
PhabricatorAuthLoginMessageType::MESSAGEKEY);
- if (!strlen($text)) {
+ if (!phutil_nonempty_string($text)) {
return null;
}
$remarkup_view = new PHUIRemarkupView($viewer, $text);
return phutil_tag(
'div',
array(
'class' => 'auth-custom-message',
),
$remarkup_view);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index 5789740e14..76c7eb9df5 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,340 +1,340 @@
<?php
final class PhabricatorAuthStartController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
// Kick the user home if they are already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
return $this->processAjaxRequest();
}
if ($request->isConduit()) {
return $this->processConduitRequest();
}
// If the user gets this far, they aren't logged in, so if they have a
// user session token we can conclude that it's invalid: if it was valid,
// they'd have been logged in above and never made it here. Try to clear
// it and warn the user they may need to nuke their cookies.
$session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
$did_clear = $request->getStr('cleared');
if (strlen($session_token)) {
$kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
$session_token);
switch ($kind) {
case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
// If this is an anonymous session. It's expected that they won't
// be logged in, so we can just continue.
break;
default:
// The session cookie is invalid, so try to clear it.
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
// We've previously tried to clear the cookie but we ended up back
// here, so it didn't work. Hard fatal instead of trying again.
if ($did_clear) {
return $this->renderError(
pht(
'Your login session is invalid, and clearing the session '.
'cookie was unsuccessful. Try clearing your browser cookies.'));
}
$redirect_uri = $request->getRequestURI();
$redirect_uri->replaceQueryParam('cleared', 1);
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
}
// If we just cleared the session cookie and it worked, clean up after
// ourselves by redirecting to get rid of the "cleared" parameter. The
// the workflow will continue normally.
if ($did_clear) {
$redirect_uri = $request->getRequestURI();
$redirect_uri->removeQueryParam('cleared');
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowLogin()) {
unset($providers[$key]);
}
}
$configs = array();
foreach ($providers as $provider) {
$configs[] = $provider->getProviderConfig();
}
if (!$providers) {
if ($this->isFirstTimeSetup()) {
// If this is a fresh install, let the user register their admin
// account.
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/register/'));
}
return $this->renderError(
pht(
'This server is not configured with any enabled authentication '.
'providers which can be used to log in. If you have accidentally '.
'locked yourself out by disabling all providers, you can use `%s` '.
'to recover access to an account.',
'./bin/auth recover <username>'));
}
$next_uri = $request->getStr('next');
- if (!strlen($next_uri)) {
+ if (!phutil_nonempty_string($next_uri)) {
if ($this->getDelegatingController()) {
// Only set a next URI from the request path if this controller was
// delegated to, which happens when a user tries to view a page which
// requires them to login.
// If this controller handled the request directly, we're on the main
// login page, and never want to redirect the user back here after they
// login.
$next_uri = (string)$this->getRequest()->getRequestURI();
}
}
if (!$request->isFormPost()) {
- if (strlen($next_uri)) {
+ if (phutil_nonempty_string($next_uri)) {
PhabricatorCookies::setNextURICookie($request, $next_uri);
}
PhabricatorCookies::setClientIDCookie($request);
}
$auto_response = $this->tryAutoLogin($providers);
if ($auto_response) {
return $auto_response;
}
$invite = $this->loadInvite();
$not_buttons = array();
$are_buttons = array();
$providers = msort($providers, 'getLoginOrder');
foreach ($providers as $provider) {
if ($invite) {
$form = $provider->buildInviteForm($this);
} else {
$form = $provider->buildLoginForm($this);
}
if ($provider->isLoginFormAButton()) {
$are_buttons[] = $form;
} else {
$not_buttons[] = $form;
}
}
$out = array();
$out[] = $not_buttons;
if ($are_buttons) {
require_celerity_resource('auth-css');
foreach ($are_buttons as $key => $button) {
$are_buttons[$key] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-button mmb',
),
$button);
}
// If we only have one button, add a second pretend button so that we
// always have two columns. This makes it easier to get the alignments
// looking reasonable.
if (count($are_buttons) == 1) {
$are_buttons[] = null;
}
$button_columns = id(new AphrontMultiColumnView())
->setFluidLayout(true);
$are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
foreach ($are_buttons as $column) {
$button_columns->addColumn($column);
}
$out[] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-buttons',
),
$button_columns);
}
$invite_message = null;
if ($invite) {
$invite_message = $this->renderInviteHeader($invite);
}
$custom_message = $this->newCustomStartMessage();
$email_login = $this->newEmailLoginView($configs);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login'));
$crumbs->setBorder(true);
$title = pht('Login');
$view = array(
$invite_message,
$custom_message,
$out,
$email_login,
);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function processAjaxRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login...".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
// Often, users end up here by clicking a disabled action link in the UI
// (for example, they might click "Edit Subtasks" on a Maniphest task
// page). After they log in we want to send them back to that main object
// page if we can, since it's confusing to end up on a standalone page with
// only a dialog (particularly if that dialog is another error,
// like a policy exception).
$via_header = AphrontRequest::getViaHeaderName();
$via_uri = AphrontRequest::getHTTPHeader($via_header);
if (strlen($via_uri)) {
PhabricatorCookies::setNextURICookie($request, $via_uri, $force = true);
}
return $this->newDialog()
->setTitle(pht('Login Required'))
->appendParagraph(pht('You must log in to take this action.'))
->addSubmitButton(pht('Log In'))
->addCancelButton('/');
}
private function processConduitRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message = pht(
'ERROR: You are making a Conduit API request to "%s", but the correct '.
'HTTP request path to use in order to access a Conduit method is "%s" '.
'(for example, "%s"). Check your configuration.',
$request_path,
$conduit_path,
$example_path);
return id(new AphrontPlainTextResponse())->setContent($message);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Failure'),
array($message));
}
private function tryAutoLogin(array $providers) {
$request = $this->getRequest();
// If the user just logged out, don't immediately log them in again.
if ($request->getURIData('loggedout')) {
return null;
}
// If we have more than one provider, we can't autologin because we
// don't know which one the user wants.
if (count($providers) != 1) {
return null;
}
$provider = head($providers);
if (!$provider->supportsAutoLogin()) {
return null;
}
$config = $provider->getProviderConfig();
if (!$config->getShouldAutoLogin()) {
return null;
}
$auto_uri = $provider->getAutoLoginURI($request);
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($auto_uri);
}
private function newEmailLoginView(array $configs) {
assert_instances_of($configs, 'PhabricatorAuthProviderConfig');
// Check if password auth is enabled. If it is, the password login form
// renders a "Forgot password?" link, so we don't need to provide a
// supplemental link.
$has_password = false;
foreach ($configs as $config) {
$provider = $config->getProvider();
if ($provider instanceof PhabricatorPasswordAuthProvider) {
$has_password = true;
}
}
if ($has_password) {
return null;
}
$view = array(
pht('Trouble logging in?'),
' ',
phutil_tag(
'a',
array(
'href' => '/login/email/',
),
pht('Send a login link to your email address.')),
);
return phutil_tag(
'div',
array(
'class' => 'auth-custom-message',
),
$view);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, May 1, 7:55 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
108916
Default Alt Text
(26 KB)

Event Timeline