2014-01-14 22:22:27 +01:00
|
|
|
<?php
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
/**
|
2014-05-01 19:23:19 +02:00
|
|
|
*
|
|
|
|
* @task use Using Sessions
|
|
|
|
* @task new Creating Sessions
|
|
|
|
* @task hisec High Security
|
|
|
|
* @task partial Partial Sessions
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
* @task onetime One Time Login URIs
|
2014-04-28 02:31:11 +02:00
|
|
|
*/
|
2014-01-14 22:22:27 +01:00
|
|
|
final class PhabricatorAuthSessionEngine extends Phobject {
|
|
|
|
|
Issue "anonymous" sessions for logged-out users
Summary:
Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes:
- First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff.
- Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance.
This does not implement CSRF yet, but gives us a client secret we can use to implement it.
Test Plan:
- Logged in.
- Logged out.
- Browsed around.
- Logged in again.
- Went through link/register.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T4310, T4339
Differential Revision: https://secure.phabricator.com/D8043
2014-01-23 23:03:22 +01:00
|
|
|
/**
|
|
|
|
* Session issued to normal users after they login through a standard channel.
|
|
|
|
* Associates the client with a standard user identity.
|
|
|
|
*/
|
|
|
|
const KIND_USER = 'U';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Session issued to users who login with some sort of credentials but do not
|
|
|
|
* have full accounts. These are sometimes called "grey users".
|
|
|
|
*
|
|
|
|
* TODO: We do not currently issue these sessions, see T4310.
|
|
|
|
*/
|
|
|
|
const KIND_EXTERNAL = 'X';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Session issued to logged-out users which has no real identity information.
|
|
|
|
* Its purpose is to protect logged-out users from CSRF.
|
|
|
|
*/
|
|
|
|
const KIND_ANONYMOUS = 'A';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Session kind isn't known.
|
|
|
|
*/
|
|
|
|
const KIND_UNKNOWN = '?';
|
|
|
|
|
|
|
|
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
/**
|
|
|
|
* Temporary tokens for one time logins.
|
|
|
|
*/
|
|
|
|
const ONETIME_TEMPORARY_TOKEN_TYPE = 'login:onetime';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Temporary tokens for password recovery after one time login.
|
|
|
|
*/
|
|
|
|
const PASSWORD_TEMPORARY_TOKEN_TYPE = 'login:password';
|
|
|
|
|
|
|
|
const ONETIME_RECOVER = 'recover';
|
|
|
|
const ONETIME_RESET = 'reset';
|
|
|
|
const ONETIME_WELCOME = 'welcome';
|
|
|
|
const ONETIME_USERNAME = 'rename';
|
|
|
|
|
|
|
|
|
Issue "anonymous" sessions for logged-out users
Summary:
Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes:
- First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff.
- Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance.
This does not implement CSRF yet, but gives us a client secret we can use to implement it.
Test Plan:
- Logged in.
- Logged out.
- Browsed around.
- Logged in again.
- Went through link/register.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T4310, T4339
Differential Revision: https://secure.phabricator.com/D8043
2014-01-23 23:03:22 +01:00
|
|
|
/**
|
|
|
|
* Get the session kind (e.g., anonymous, user, external account) from a
|
|
|
|
* session token. Returns a `KIND_` constant.
|
|
|
|
*
|
|
|
|
* @param string Session token.
|
|
|
|
* @return const Session kind constant.
|
|
|
|
*/
|
|
|
|
public static function getSessionKindFromToken($session_token) {
|
|
|
|
if (strpos($session_token, '/') === false) {
|
|
|
|
// Old-style session, these are all user sessions.
|
|
|
|
return self::KIND_USER;
|
|
|
|
}
|
|
|
|
|
|
|
|
list($kind, $key) = explode('/', $session_token, 2);
|
|
|
|
|
|
|
|
switch ($kind) {
|
|
|
|
case self::KIND_ANONYMOUS:
|
|
|
|
case self::KIND_USER:
|
|
|
|
case self::KIND_EXTERNAL:
|
|
|
|
return $kind;
|
|
|
|
default:
|
|
|
|
return self::KIND_UNKNOWN;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-05-01 19:23:19 +02:00
|
|
|
/**
|
|
|
|
* Load the user identity associated with a session of a given type,
|
|
|
|
* identified by token.
|
|
|
|
*
|
|
|
|
* When the user presents a session token to an API, this method verifies
|
|
|
|
* it is of the correct type and loads the corresponding identity if the
|
|
|
|
* session exists and is valid.
|
|
|
|
*
|
|
|
|
* NOTE: `$session_type` is the type of session that is required by the
|
|
|
|
* loading context. This prevents use of a Conduit sesssion as a Web
|
|
|
|
* session, for example.
|
|
|
|
*
|
|
|
|
* @param const The type of session to load.
|
|
|
|
* @param string The session token.
|
|
|
|
* @return PhabricatorUser|null
|
|
|
|
* @task use
|
|
|
|
*/
|
Issue "anonymous" sessions for logged-out users
Summary:
Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes:
- First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff.
- Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance.
This does not implement CSRF yet, but gives us a client secret we can use to implement it.
Test Plan:
- Logged in.
- Logged out.
- Browsed around.
- Logged in again.
- Went through link/register.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T4310, T4339
Differential Revision: https://secure.phabricator.com/D8043
2014-01-23 23:03:22 +01:00
|
|
|
public function loadUserForSession($session_type, $session_token) {
|
|
|
|
$session_kind = self::getSessionKindFromToken($session_token);
|
|
|
|
switch ($session_kind) {
|
|
|
|
case self::KIND_ANONYMOUS:
|
|
|
|
// Don't bother trying to load a user for an anonymous session, since
|
|
|
|
// neither the session nor the user exist.
|
|
|
|
return null;
|
|
|
|
case self::KIND_UNKNOWN:
|
|
|
|
// If we don't know what kind of session this is, don't go looking for
|
|
|
|
// it.
|
|
|
|
return null;
|
|
|
|
case self::KIND_USER:
|
|
|
|
break;
|
|
|
|
case self::KIND_EXTERNAL:
|
|
|
|
// TODO: Implement these (T4310).
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2014-01-14 22:22:27 +01:00
|
|
|
$session_table = new PhabricatorAuthSession();
|
|
|
|
$user_table = new PhabricatorUser();
|
2014-01-15 22:56:16 +01:00
|
|
|
$conn_r = $session_table->establishConnection('r');
|
2014-04-28 02:31:11 +02:00
|
|
|
$session_key = PhabricatorHash::digest($session_token);
|
2014-01-15 22:56:16 +01:00
|
|
|
|
|
|
|
// NOTE: We're being clever here because this happens on every page load,
|
2014-04-28 02:31:11 +02:00
|
|
|
// and by joining we can save a query. This might be getting too clever
|
|
|
|
// for its own good, though...
|
2014-01-14 22:22:27 +01:00
|
|
|
|
|
|
|
$info = queryfx_one(
|
|
|
|
$conn_r,
|
2014-04-28 02:31:11 +02:00
|
|
|
'SELECT
|
|
|
|
s.id AS s_id,
|
|
|
|
s.sessionExpires AS s_sessionExpires,
|
|
|
|
s.sessionStart AS s_sessionStart,
|
|
|
|
s.highSecurityUntil AS s_highSecurityUntil,
|
2014-05-01 19:23:02 +02:00
|
|
|
s.isPartial AS s_isPartial,
|
2015-02-13 00:22:56 +01:00
|
|
|
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
|
2014-04-28 02:31:11 +02:00
|
|
|
u.*
|
2014-01-15 22:56:16 +01:00
|
|
|
FROM %T u JOIN %T s ON u.phid = s.userPHID
|
2014-01-16 02:27:59 +01:00
|
|
|
AND s.type = %s AND s.sessionKey = %s',
|
2014-01-14 22:22:27 +01:00
|
|
|
$user_table->getTableName(),
|
|
|
|
$session_table->getTableName(),
|
2014-01-16 02:27:59 +01:00
|
|
|
$session_type,
|
2014-04-28 02:31:11 +02:00
|
|
|
$session_key);
|
2014-01-14 22:22:27 +01:00
|
|
|
|
|
|
|
if (!$info) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
$session_dict = array(
|
|
|
|
'userPHID' => $info['phid'],
|
|
|
|
'sessionKey' => $session_key,
|
|
|
|
'type' => $session_type,
|
|
|
|
);
|
|
|
|
foreach ($info as $key => $value) {
|
|
|
|
if (strncmp($key, 's_', 2) === 0) {
|
|
|
|
unset($info[$key]);
|
|
|
|
$session_dict[substr($key, 2)] = $value;
|
|
|
|
}
|
|
|
|
}
|
2015-06-02 17:52:00 +02:00
|
|
|
|
|
|
|
$user = $user_table->loadFromArray($info);
|
|
|
|
switch ($session_type) {
|
|
|
|
case PhabricatorAuthSession::TYPE_WEB:
|
|
|
|
// Explicitly prevent bots and mailing lists from establishing web
|
|
|
|
// sessions. It's normally impossible to attach authentication to these
|
|
|
|
// accounts, and likewise impossible to generate sessions, but it's
|
|
|
|
// technically possible that a session could exist in the database. If
|
|
|
|
// one does somehow, refuse to load it.
|
|
|
|
if (!$user->canEstablishWebSessions()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
|
2014-01-15 22:56:16 +01:00
|
|
|
|
|
|
|
$ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
|
|
|
|
|
|
|
|
// If more than 20% of the time on this session has been used, refresh the
|
|
|
|
// TTL back up to the full duration. The idea here is that sessions are
|
|
|
|
// good forever if used regularly, but get GC'd when they fall out of use.
|
|
|
|
|
2014-08-04 21:04:47 +02:00
|
|
|
// NOTE: If we begin rotating session keys when extending sessions, the
|
|
|
|
// CSRF code needs to be updated so CSRF tokens survive session rotation.
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
if (time() + (0.80 * $ttl) > $session->getSessionExpires()) {
|
2014-01-15 22:56:16 +01:00
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$conn_w = $session_table->establishConnection('w');
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d',
|
2014-04-28 02:31:11 +02:00
|
|
|
$session->getTableName(),
|
2014-01-15 22:56:16 +01:00
|
|
|
$ttl,
|
2014-04-28 02:31:11 +02:00
|
|
|
$session->getID());
|
2014-01-15 22:56:16 +01:00
|
|
|
unset($unguarded);
|
|
|
|
}
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
$user->attachSession($session);
|
|
|
|
return $user;
|
2014-01-14 22:22:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Issue a new session key for a given identity. Phabricator supports
|
|
|
|
* different types of sessions (like "web" and "conduit") and each session
|
|
|
|
* type may have multiple concurrent sessions (this allows a user to be
|
|
|
|
* logged in on multiple browsers at the same time, for instance).
|
|
|
|
*
|
|
|
|
* Note that this method is transport-agnostic and does not set cookies or
|
|
|
|
* issue other types of tokens, it ONLY generates a new session key.
|
|
|
|
*
|
|
|
|
* You can configure the maximum number of concurrent sessions for various
|
|
|
|
* session types in the Phabricator configuration.
|
|
|
|
*
|
Issue "anonymous" sessions for logged-out users
Summary:
Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes:
- First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff.
- Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance.
This does not implement CSRF yet, but gives us a client secret we can use to implement it.
Test Plan:
- Logged in.
- Logged out.
- Browsed around.
- Logged in again.
- Went through link/register.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T4310, T4339
Differential Revision: https://secure.phabricator.com/D8043
2014-01-23 23:03:22 +01:00
|
|
|
* @param const Session type constant (see
|
|
|
|
* @{class:PhabricatorAuthSession}).
|
|
|
|
* @param phid|null Identity to establish a session for, usually a user
|
|
|
|
* PHID. With `null`, generates an anonymous session.
|
2014-05-01 19:23:02 +02:00
|
|
|
* @param bool True to issue a partial session.
|
Issue "anonymous" sessions for logged-out users
Summary:
Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes:
- First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff.
- Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance.
This does not implement CSRF yet, but gives us a client secret we can use to implement it.
Test Plan:
- Logged in.
- Logged out.
- Browsed around.
- Logged in again.
- Went through link/register.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T4310, T4339
Differential Revision: https://secure.phabricator.com/D8043
2014-01-23 23:03:22 +01:00
|
|
|
* @return string Newly generated session key.
|
2014-01-14 22:22:27 +01:00
|
|
|
*/
|
2014-05-01 19:23:02 +02:00
|
|
|
public function establishSession($session_type, $identity_phid, $partial) {
|
2014-01-14 22:22:27 +01:00
|
|
|
// Consume entropy to generate a new session key, forestalling the eventual
|
|
|
|
// heat death of the universe.
|
|
|
|
$session_key = Filesystem::readRandomCharacters(40);
|
|
|
|
|
Issue "anonymous" sessions for logged-out users
Summary:
Ref T4339. Ref T4310. Currently, sessions look like `"afad85d675fda87a4fadd54"`, and are only issued for logged-in users. To support logged-out CSRF and (eventually) external user sessions, I made two small changes:
- First, sessions now have a "kind", which is indicated by a prefix, like `"A/ab987asdcas7dca"`. This mostly allows us to issue session queries more efficiently: we don't have to issue a query at all for anonymous sessions, and can join the correct table for user and external sessions and save a query. Generally, this gives us more debugging information and more opportunity to recover from issues in a user-friendly way, as with the "invalid session" error in this diff.
- Secondly, if you load a page and don't have a session, we give you an anonymous session. This is just a secret with no special significance.
This does not implement CSRF yet, but gives us a client secret we can use to implement it.
Test Plan:
- Logged in.
- Logged out.
- Browsed around.
- Logged in again.
- Went through link/register.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T4310, T4339
Differential Revision: https://secure.phabricator.com/D8043
2014-01-23 23:03:22 +01:00
|
|
|
if ($identity_phid === null) {
|
|
|
|
return self::KIND_ANONYMOUS.'/'.$session_key;
|
|
|
|
}
|
|
|
|
|
|
|
|
$session_table = new PhabricatorAuthSession();
|
|
|
|
$conn_w = $session_table->establishConnection('w');
|
|
|
|
|
2014-01-16 02:27:59 +01:00
|
|
|
// This has a side effect of validating the session type.
|
|
|
|
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
|
2014-01-14 22:22:27 +01:00
|
|
|
|
2014-05-01 19:23:02 +02:00
|
|
|
$digest_key = PhabricatorHash::digest($session_key);
|
|
|
|
|
2014-01-16 02:27:59 +01:00
|
|
|
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
|
|
|
|
// write.
|
2014-01-14 22:22:27 +01:00
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
2014-01-16 02:27:59 +01:00
|
|
|
id(new PhabricatorAuthSession())
|
|
|
|
->setUserPHID($identity_phid)
|
|
|
|
->setType($session_type)
|
2014-05-01 19:23:02 +02:00
|
|
|
->setSessionKey($digest_key)
|
2014-01-16 02:27:59 +01:00
|
|
|
->setSessionStart(time())
|
|
|
|
->setSessionExpires(time() + $session_ttl)
|
2014-05-01 19:23:02 +02:00
|
|
|
->setIsPartial($partial ? 1 : 0)
|
2015-02-13 00:22:56 +01:00
|
|
|
->setSignedLegalpadDocuments(0)
|
2014-01-16 02:27:59 +01:00
|
|
|
->save();
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::initializeNewLog(
|
|
|
|
null,
|
2014-01-14 22:22:27 +01:00
|
|
|
$identity_phid,
|
2014-05-01 19:23:02 +02:00
|
|
|
($partial
|
|
|
|
? PhabricatorUserLog::ACTION_LOGIN_PARTIAL
|
|
|
|
: PhabricatorUserLog::ACTION_LOGIN));
|
|
|
|
|
2014-01-16 02:27:59 +01:00
|
|
|
$log->setDetails(
|
|
|
|
array(
|
|
|
|
'session_type' => $session_type,
|
|
|
|
));
|
2014-05-01 19:23:02 +02:00
|
|
|
$log->setSession($digest_key);
|
2014-01-16 02:27:59 +01:00
|
|
|
$log->save();
|
|
|
|
unset($unguarded);
|
2014-01-14 22:22:27 +01:00
|
|
|
|
|
|
|
return $session_key;
|
|
|
|
}
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
|
Terminate other sessions on credential changes
Summary:
Fixes T5509. Currently, existing sessions live on even if you change your password.
Over the course of the program, we've recieved a lot of HackerOne reports that sessions do not terminate when users change their passwords. I hold that this isn't a security vulnerability: users can explicitly manage sessions, and this is more general and more powerful than tying session termination to password resets. In particular, many installs do not use a password provider at all (and no researcher has reported this in a general, application-aware way that discusses multiple authentication providers).
That said, dealing with these false positives is vaguely time consuming, and the "expected" behavior isn't bad for users, so just align behavior with researcher expectations: when passwords are changed, providers are removed, or multi-factor authentication is added to an account, terminate all other active login sessions.
Test Plan:
- Using two browsers, established multiple login sessions.
- In one browser, changed account password. Saw session terminate and logout in the second browser.
- In one browser, removed an authentication provider. Saw session terminate and logout in the second browser.
- In one browser, added MFA. Saw session terminate and logout in the second browser.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5509
Differential Revision: https://secure.phabricator.com/D10135
2014-08-04 21:04:35 +02:00
|
|
|
/**
|
|
|
|
* Terminate all of a user's login sessions.
|
|
|
|
*
|
|
|
|
* This is used when users change passwords, linked accounts, or add
|
|
|
|
* multifactor authentication.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser User whose sessions should be terminated.
|
|
|
|
* @param string|null Optionally, one session to keep. Normally, the current
|
|
|
|
* login session.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function terminateLoginSessions(
|
|
|
|
PhabricatorUser $user,
|
|
|
|
$except_session = null) {
|
|
|
|
|
|
|
|
$sessions = id(new PhabricatorAuthSessionQuery())
|
|
|
|
->setViewer($user)
|
|
|
|
->withIdentityPHIDs(array($user->getPHID()))
|
|
|
|
->execute();
|
|
|
|
|
|
|
|
if ($except_session !== null) {
|
|
|
|
$except_session = PhabricatorHash::digest($except_session);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($sessions as $key => $session) {
|
|
|
|
if ($except_session !== null) {
|
2015-09-02 00:52:44 +02:00
|
|
|
$is_except = phutil_hashes_are_identical(
|
|
|
|
$session->getSessionKey(),
|
|
|
|
$except_session);
|
|
|
|
if ($is_except) {
|
Terminate other sessions on credential changes
Summary:
Fixes T5509. Currently, existing sessions live on even if you change your password.
Over the course of the program, we've recieved a lot of HackerOne reports that sessions do not terminate when users change their passwords. I hold that this isn't a security vulnerability: users can explicitly manage sessions, and this is more general and more powerful than tying session termination to password resets. In particular, many installs do not use a password provider at all (and no researcher has reported this in a general, application-aware way that discusses multiple authentication providers).
That said, dealing with these false positives is vaguely time consuming, and the "expected" behavior isn't bad for users, so just align behavior with researcher expectations: when passwords are changed, providers are removed, or multi-factor authentication is added to an account, terminate all other active login sessions.
Test Plan:
- Using two browsers, established multiple login sessions.
- In one browser, changed account password. Saw session terminate and logout in the second browser.
- In one browser, removed an authentication provider. Saw session terminate and logout in the second browser.
- In one browser, added MFA. Saw session terminate and logout in the second browser.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5509
Differential Revision: https://secure.phabricator.com/D10135
2014-08-04 21:04:35 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$session->delete();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-05-01 19:23:19 +02:00
|
|
|
/* -( High Security )------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
/**
|
|
|
|
* Require high security, or prompt the user to enter high security.
|
|
|
|
*
|
|
|
|
* If the user's session is in high security, this method will return a
|
|
|
|
* token. Otherwise, it will throw an exception which will eventually
|
|
|
|
* be converted into a multi-factor authentication workflow.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser User whose session needs to be in high security.
|
|
|
|
* @param AphrontReqeust Current request.
|
|
|
|
* @param string URI to return the user to if they cancel.
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
* @param bool True to jump partial sessions directly into high
|
|
|
|
* security instead of just upgrading them to full
|
|
|
|
* sessions.
|
2014-04-28 02:31:11 +02:00
|
|
|
* @return PhabricatorAuthHighSecurityToken Security token.
|
2014-05-01 19:23:19 +02:00
|
|
|
* @task hisec
|
2014-04-28 02:31:11 +02:00
|
|
|
*/
|
|
|
|
public function requireHighSecuritySession(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
AphrontRequest $request,
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
$cancel_uri,
|
|
|
|
$jump_into_hisec = false) {
|
2014-04-28 02:31:11 +02:00
|
|
|
|
|
|
|
if (!$viewer->hasSession()) {
|
|
|
|
throw new Exception(
|
|
|
|
pht('Requiring a high-security session from a user with no session!'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$session = $viewer->getSession();
|
|
|
|
|
2014-04-28 19:20:54 +02:00
|
|
|
// Check if the session is already in high security mode.
|
2014-04-28 02:31:11 +02:00
|
|
|
$token = $this->issueHighSecurityToken($session);
|
|
|
|
if ($token) {
|
|
|
|
return $token;
|
|
|
|
}
|
|
|
|
|
2014-04-28 19:20:54 +02:00
|
|
|
// Load the multi-factor auth sources attached to this account.
|
|
|
|
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
|
|
|
|
'userPHID = %s',
|
|
|
|
$viewer->getPHID());
|
|
|
|
|
|
|
|
// If the account has no associated multi-factor auth, just issue a token
|
|
|
|
// without putting the session into high security mode. This is generally
|
|
|
|
// easier for users. A minor but desirable side effect is that when a user
|
|
|
|
// adds an auth factor, existing sessions won't get a free pass into hisec,
|
|
|
|
// since they never actually got marked as hisec.
|
|
|
|
if (!$factors) {
|
|
|
|
return $this->issueHighSecurityToken($session, true);
|
|
|
|
}
|
|
|
|
|
2014-04-30 23:30:31 +02:00
|
|
|
// Check for a rate limit without awarding points, so the user doesn't
|
|
|
|
// get partway through the workflow only to get blocked.
|
|
|
|
PhabricatorSystemActionEngine::willTakeAction(
|
|
|
|
array($viewer->getPHID()),
|
|
|
|
new PhabricatorAuthTryFactorAction(),
|
|
|
|
0);
|
|
|
|
|
2014-04-28 19:20:54 +02:00
|
|
|
$validation_results = array();
|
2014-04-28 02:31:11 +02:00
|
|
|
if ($request->isHTTPPost()) {
|
|
|
|
$request->validateCSRF();
|
|
|
|
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
|
|
|
|
|
2014-04-30 23:30:31 +02:00
|
|
|
// Limit factor verification rates to prevent brute force attacks.
|
|
|
|
PhabricatorSystemActionEngine::willTakeAction(
|
|
|
|
array($viewer->getPHID()),
|
|
|
|
new PhabricatorAuthTryFactorAction(),
|
|
|
|
1);
|
|
|
|
|
2014-04-28 19:20:54 +02:00
|
|
|
$ok = true;
|
|
|
|
foreach ($factors as $factor) {
|
|
|
|
$id = $factor->getID();
|
|
|
|
$impl = $factor->requireImplementation();
|
|
|
|
|
|
|
|
$validation_results[$id] = $impl->processValidateFactorForm(
|
|
|
|
$factor,
|
|
|
|
$viewer,
|
|
|
|
$request);
|
|
|
|
|
|
|
|
if (!$impl->isFactorValid($factor, $validation_results[$id])) {
|
|
|
|
$ok = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ok) {
|
2014-04-30 23:30:31 +02:00
|
|
|
// Give the user a credit back for a successful factor verification.
|
|
|
|
PhabricatorSystemActionEngine::willTakeAction(
|
|
|
|
array($viewer->getPHID()),
|
|
|
|
new PhabricatorAuthTryFactorAction(),
|
|
|
|
-1);
|
|
|
|
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
if ($session->getIsPartial() && !$jump_into_hisec) {
|
|
|
|
// If we have a partial session and are not jumping directly into
|
|
|
|
// hisec, just issue a token without putting it in high security
|
|
|
|
// mode.
|
2014-05-01 19:23:02 +02:00
|
|
|
return $this->issueHighSecurityToken($session, true);
|
|
|
|
}
|
|
|
|
|
2014-04-28 19:20:54 +02:00
|
|
|
$until = time() + phutil_units('15 minutes in seconds');
|
|
|
|
$session->setHighSecurityUntil($until);
|
|
|
|
|
|
|
|
queryfx(
|
|
|
|
$session->establishConnection('w'),
|
|
|
|
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
|
|
|
|
$session->getTableName(),
|
|
|
|
$until,
|
|
|
|
$session->getID());
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::initializeNewLog(
|
|
|
|
$viewer,
|
|
|
|
$viewer->getPHID(),
|
|
|
|
PhabricatorUserLog::ACTION_ENTER_HISEC);
|
|
|
|
$log->save();
|
|
|
|
} else {
|
|
|
|
$log = PhabricatorUserLog::initializeNewLog(
|
|
|
|
$viewer,
|
|
|
|
$viewer->getPHID(),
|
|
|
|
PhabricatorUserLog::ACTION_FAIL_HISEC);
|
|
|
|
$log->save();
|
|
|
|
}
|
2014-04-28 02:31:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$token = $this->issueHighSecurityToken($session);
|
|
|
|
if ($token) {
|
|
|
|
return $token;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw id(new PhabricatorAuthHighSecurityRequiredException())
|
2014-04-28 19:20:54 +02:00
|
|
|
->setCancelURI($cancel_uri)
|
|
|
|
->setFactors($factors)
|
|
|
|
->setFactorValidationResults($validation_results);
|
2014-04-28 02:31:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Issue a high security token for a session, if authorized.
|
|
|
|
*
|
|
|
|
* @param PhabricatorAuthSession Session to issue a token for.
|
2014-05-01 19:23:02 +02:00
|
|
|
* @param bool Force token issue.
|
2014-04-28 02:31:11 +02:00
|
|
|
* @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
|
2014-05-01 19:23:19 +02:00
|
|
|
* @task hisec
|
2014-04-28 02:31:11 +02:00
|
|
|
*/
|
2014-05-01 19:23:02 +02:00
|
|
|
private function issueHighSecurityToken(
|
|
|
|
PhabricatorAuthSession $session,
|
|
|
|
$force = false) {
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
$until = $session->getHighSecurityUntil();
|
2014-05-01 19:23:02 +02:00
|
|
|
if ($until > time() || $force) {
|
2014-04-28 02:31:11 +02:00
|
|
|
return new PhabricatorAuthHighSecurityToken();
|
|
|
|
}
|
2014-05-01 19:23:19 +02:00
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render a form for providing relevant multi-factor credentials.
|
|
|
|
*
|
2014-05-01 19:23:19 +02:00
|
|
|
* @param PhabricatorUser Viewing user.
|
|
|
|
* @param AphrontRequest Current request.
|
|
|
|
* @return AphrontFormView Renderable form.
|
|
|
|
* @task hisec
|
2014-04-28 02:31:11 +02:00
|
|
|
*/
|
|
|
|
public function renderHighSecurityForm(
|
2014-04-28 19:20:54 +02:00
|
|
|
array $factors,
|
|
|
|
array $validation_results,
|
2014-04-28 02:31:11 +02:00
|
|
|
PhabricatorUser $viewer,
|
|
|
|
AphrontRequest $request) {
|
|
|
|
|
|
|
|
$form = id(new AphrontFormView())
|
|
|
|
->setUser($viewer)
|
|
|
|
->appendRemarkupInstructions('');
|
|
|
|
|
2014-04-28 19:20:54 +02:00
|
|
|
foreach ($factors as $factor) {
|
|
|
|
$factor->requireImplementation()->renderValidateFactorForm(
|
|
|
|
$factor,
|
|
|
|
$form,
|
|
|
|
$viewer,
|
|
|
|
idx($validation_results, $factor->getID()));
|
|
|
|
}
|
|
|
|
|
|
|
|
$form->appendRemarkupInstructions('');
|
|
|
|
|
2014-04-28 02:31:11 +02:00
|
|
|
return $form;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-05-01 19:23:19 +02:00
|
|
|
/**
|
|
|
|
* Strip the high security flag from a session.
|
|
|
|
*
|
|
|
|
* Kicks a session out of high security and logs the exit.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser Acting user.
|
|
|
|
* @param PhabricatorAuthSession Session to return to normal security.
|
|
|
|
* @return void
|
|
|
|
* @task hisec
|
|
|
|
*/
|
2014-04-28 02:32:09 +02:00
|
|
|
public function exitHighSecurity(
|
|
|
|
PhabricatorUser $viewer,
|
|
|
|
PhabricatorAuthSession $session) {
|
|
|
|
|
2014-05-01 19:23:19 +02:00
|
|
|
if (!$session->getHighSecurityUntil()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-04-28 02:32:09 +02:00
|
|
|
queryfx(
|
|
|
|
$session->establishConnection('w'),
|
|
|
|
'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
|
|
|
|
$session->getTableName(),
|
|
|
|
$session->getID());
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::initializeNewLog(
|
|
|
|
$viewer,
|
|
|
|
$viewer->getPHID(),
|
|
|
|
PhabricatorUserLog::ACTION_EXIT_HISEC);
|
|
|
|
$log->save();
|
|
|
|
}
|
|
|
|
|
2014-05-01 19:23:02 +02:00
|
|
|
|
2014-05-01 19:23:19 +02:00
|
|
|
/* -( Partial Sessions )--------------------------------------------------- */
|
|
|
|
|
|
|
|
|
2014-05-01 19:23:02 +02:00
|
|
|
/**
|
|
|
|
* Upgrade a partial session to a full session.
|
|
|
|
*
|
|
|
|
* @param PhabricatorAuthSession Session to upgrade.
|
|
|
|
* @return void
|
2014-05-01 19:23:19 +02:00
|
|
|
* @task partial
|
2014-05-01 19:23:02 +02:00
|
|
|
*/
|
|
|
|
public function upgradePartialSession(PhabricatorUser $viewer) {
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
|
2014-05-01 19:23:02 +02:00
|
|
|
if (!$viewer->hasSession()) {
|
|
|
|
throw new Exception(
|
|
|
|
pht('Upgrading partial session of user with no session!'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$session = $viewer->getSession();
|
|
|
|
|
|
|
|
if (!$session->getIsPartial()) {
|
|
|
|
throw new Exception(pht('Session is not partial!'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$session->setIsPartial(0);
|
|
|
|
|
|
|
|
queryfx(
|
|
|
|
$session->establishConnection('w'),
|
|
|
|
'UPDATE %T SET isPartial = %d WHERE id = %d',
|
|
|
|
$session->getTableName(),
|
|
|
|
0,
|
|
|
|
$session->getID());
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::initializeNewLog(
|
|
|
|
$viewer,
|
|
|
|
$viewer->getPHID(),
|
|
|
|
PhabricatorUserLog::ACTION_LOGIN_FULL);
|
|
|
|
$log->save();
|
|
|
|
unset($unguarded);
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-02-13 00:22:56 +01:00
|
|
|
/* -( Legalpad Documents )-------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Upgrade a session to have all legalpad documents signed.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser User whose session should upgrade.
|
|
|
|
* @param array LegalpadDocument objects
|
|
|
|
* @return void
|
|
|
|
* @task partial
|
|
|
|
*/
|
|
|
|
public function signLegalpadDocuments(PhabricatorUser $viewer, array $docs) {
|
|
|
|
|
|
|
|
if (!$viewer->hasSession()) {
|
|
|
|
throw new Exception(
|
|
|
|
pht('Signing session legalpad documents of user with no session!'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$session = $viewer->getSession();
|
|
|
|
|
|
|
|
if ($session->getSignedLegalpadDocuments()) {
|
|
|
|
throw new Exception(pht(
|
|
|
|
'Session has already signed required legalpad documents!'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$session->setSignedLegalpadDocuments(1);
|
|
|
|
|
|
|
|
queryfx(
|
|
|
|
$session->establishConnection('w'),
|
|
|
|
'UPDATE %T SET signedLegalpadDocuments = %d WHERE id = %d',
|
|
|
|
$session->getTableName(),
|
|
|
|
1,
|
|
|
|
$session->getID());
|
|
|
|
|
|
|
|
if (!empty($docs)) {
|
|
|
|
$log = PhabricatorUserLog::initializeNewLog(
|
|
|
|
$viewer,
|
|
|
|
$viewer->getPHID(),
|
|
|
|
PhabricatorUserLog::ACTION_LOGIN_LEGALPAD);
|
|
|
|
$log->save();
|
|
|
|
}
|
|
|
|
unset($unguarded);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
/* -( One Time Login URIs )------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve a temporary, one-time URI which can log in to an account.
|
|
|
|
*
|
|
|
|
* These URIs are used for password recovery and to regain access to accounts
|
|
|
|
* which users have been locked out of.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser User to generate a URI for.
|
|
|
|
* @param PhabricatorUserEmail Optionally, email to verify when
|
|
|
|
* link is used.
|
|
|
|
* @param string Optional context string for the URI. This is purely cosmetic
|
|
|
|
* and used only to customize workflow and error messages.
|
|
|
|
* @return string Login URI.
|
|
|
|
* @task onetime
|
|
|
|
*/
|
|
|
|
public function getOneTimeLoginURI(
|
|
|
|
PhabricatorUser $user,
|
|
|
|
PhabricatorUserEmail $email = null,
|
|
|
|
$type = self::ONETIME_RESET) {
|
|
|
|
|
|
|
|
$key = Filesystem::readRandomCharacters(32);
|
|
|
|
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
|
|
|
|
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
id(new PhabricatorAuthTemporaryToken())
|
|
|
|
->setObjectPHID($user->getPHID())
|
|
|
|
->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
|
|
|
|
->setTokenExpires(time() + phutil_units('1 day in seconds'))
|
|
|
|
->setTokenCode($key_hash)
|
|
|
|
->save();
|
|
|
|
unset($unguarded);
|
|
|
|
|
|
|
|
$uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/';
|
|
|
|
if ($email) {
|
|
|
|
$uri = $uri.$email->getID().'/';
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$uri = PhabricatorEnv::getProductionURI($uri);
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
// If a user runs `bin/auth recover` before configuring the base URI,
|
|
|
|
// just show the path. We don't have any way to figure out the domain.
|
|
|
|
// See T4132.
|
|
|
|
}
|
|
|
|
|
|
|
|
return $uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load the temporary token associated with a given one-time login key.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser User to load the token for.
|
|
|
|
* @param PhabricatorUserEmail Optionally, email to verify when
|
|
|
|
* link is used.
|
|
|
|
* @param string Key user is presenting as a valid one-time login key.
|
|
|
|
* @return PhabricatorAuthTemporaryToken|null Token, if one exists.
|
|
|
|
* @task onetime
|
|
|
|
*/
|
|
|
|
public function loadOneTimeLoginKey(
|
|
|
|
PhabricatorUser $user,
|
|
|
|
PhabricatorUserEmail $email = null,
|
|
|
|
$key = null) {
|
|
|
|
|
|
|
|
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
|
|
|
|
|
|
|
|
return id(new PhabricatorAuthTemporaryTokenQuery())
|
|
|
|
->setViewer($user)
|
|
|
|
->withObjectPHIDs(array($user->getPHID()))
|
|
|
|
->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
|
|
|
|
->withTokenCodes(array($key_hash))
|
|
|
|
->withExpired(false)
|
|
|
|
->executeOne();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Hash a one-time login key for storage as a temporary token.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser User this key is for.
|
|
|
|
* @param PhabricatorUserEmail Optionally, email to verify when
|
|
|
|
* link is used.
|
|
|
|
* @param string The one time login key.
|
|
|
|
* @return string Hash of the key.
|
|
|
|
* task onetime
|
|
|
|
*/
|
|
|
|
private function getOneTimeLoginKeyHash(
|
|
|
|
PhabricatorUser $user,
|
|
|
|
PhabricatorUserEmail $email = null,
|
|
|
|
$key = null) {
|
|
|
|
|
|
|
|
$parts = array(
|
|
|
|
$key,
|
|
|
|
$user->getAccountSecret(),
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($email) {
|
|
|
|
$parts[] = $email->getVerificationCode();
|
|
|
|
}
|
2014-05-01 19:23:02 +02:00
|
|
|
|
Make password reset emails use one-time tokens
Summary:
Ref T4398. This code hadn't been touched in a while and had a few crufty bits.
**One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne.
Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours.
**Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address.
This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator.
However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification.
Instead, make the email verification part optional, and use it on welcome links but not other types of links.
**Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow.
**Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it.
Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful.
Mitigate this in two ways:
- First, send to the actual email address the user entered, not the primary account email address.
- Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable).
**Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one.
Previously, this code was static and based on the email address. Instead, issue a one-time code.
**Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble.
However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row.
To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance.
**Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit.
Test Plan:
- **One Time Resets**
- Used a reset link.
- Tried to reuse a reset link, got denied.
- Verified each link is different.
- **Coupling of Email Verification and One-Time Login**
- Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component.
- Tried to tack one on, got denied.
- Used the welcome email link to login + verify.
- Tried to mutate the URI to not verify, or verify something else: got denied.
- **Message Customization**
- Viewed messages on the different workflows. They seemed OK.
- **Reset Emails Going to Main Account Email**
- Sent password reset email to non-primary email.
- Received email at specified address.
- Verified it does not verify the address.
- **Password Reset Without Old Password**
- Reset password without knowledge of old one after email reset.
- Tried to do that without a key, got denied.
- Tried to reuse a key, got denied.
- **Jump Into Hisec**
- Logged in with MFA user, got factor'd, jumped directly into hisec.
- Logged in with non-MFA user, no factors, normal password reset.
- **Some UI Cleanup**
- Viewed new UI.
- **Misc**
- Created accounts, logged in with welcome link, got verified.
- Changed a username, used link to log back in.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4398
Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 19:41:00 +02:00
|
|
|
return PhabricatorHash::digest(implode(':', $parts));
|
2014-05-01 19:23:02 +02:00
|
|
|
}
|
|
|
|
|
2014-01-14 22:22:27 +01:00
|
|
|
}
|