[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/auth/engine/ -> PhabricatorAuthSessionEngine.php (source)

   1  <?php
   2  
   3  /**
   4   *
   5   * @task use      Using Sessions
   6   * @task new      Creating Sessions
   7   * @task hisec    High Security
   8   * @task partial  Partial Sessions
   9   * @task onetime  One Time Login URIs
  10   */
  11  final class PhabricatorAuthSessionEngine extends Phobject {
  12  
  13    /**
  14     * Session issued to normal users after they login through a standard channel.
  15     * Associates the client with a standard user identity.
  16     */
  17    const KIND_USER      = 'U';
  18  
  19  
  20    /**
  21     * Session issued to users who login with some sort of credentials but do not
  22     * have full accounts. These are sometimes called "grey users".
  23     *
  24     * TODO: We do not currently issue these sessions, see T4310.
  25     */
  26    const KIND_EXTERNAL  = 'X';
  27  
  28  
  29    /**
  30     * Session issued to logged-out users which has no real identity information.
  31     * Its purpose is to protect logged-out users from CSRF.
  32     */
  33    const KIND_ANONYMOUS = 'A';
  34  
  35  
  36    /**
  37     * Session kind isn't known.
  38     */
  39    const KIND_UNKNOWN   = '?';
  40  
  41  
  42    /**
  43     * Temporary tokens for one time logins.
  44     */
  45    const ONETIME_TEMPORARY_TOKEN_TYPE = 'login:onetime';
  46  
  47  
  48    /**
  49     * Temporary tokens for password recovery after one time login.
  50     */
  51    const PASSWORD_TEMPORARY_TOKEN_TYPE = 'login:password';
  52  
  53    const ONETIME_RECOVER = 'recover';
  54    const ONETIME_RESET = 'reset';
  55    const ONETIME_WELCOME = 'welcome';
  56    const ONETIME_USERNAME = 'rename';
  57  
  58  
  59    /**
  60     * Get the session kind (e.g., anonymous, user, external account) from a
  61     * session token. Returns a `KIND_` constant.
  62     *
  63     * @param   string  Session token.
  64     * @return  const   Session kind constant.
  65     */
  66    public static function getSessionKindFromToken($session_token) {
  67      if (strpos($session_token, '/') === false) {
  68        // Old-style session, these are all user sessions.
  69        return self::KIND_USER;
  70      }
  71  
  72      list($kind, $key) = explode('/', $session_token, 2);
  73  
  74      switch ($kind) {
  75        case self::KIND_ANONYMOUS:
  76        case self::KIND_USER:
  77        case self::KIND_EXTERNAL:
  78          return $kind;
  79        default:
  80          return self::KIND_UNKNOWN;
  81      }
  82    }
  83  
  84  
  85    /**
  86     * Load the user identity associated with a session of a given type,
  87     * identified by token.
  88     *
  89     * When the user presents a session token to an API, this method verifies
  90     * it is of the correct type and loads the corresponding identity if the
  91     * session exists and is valid.
  92     *
  93     * NOTE: `$session_type` is the type of session that is required by the
  94     * loading context. This prevents use of a Conduit sesssion as a Web
  95     * session, for example.
  96     *
  97     * @param const The type of session to load.
  98     * @param string The session token.
  99     * @return PhabricatorUser|null
 100     * @task use
 101     */
 102    public function loadUserForSession($session_type, $session_token) {
 103      $session_kind = self::getSessionKindFromToken($session_token);
 104      switch ($session_kind) {
 105        case self::KIND_ANONYMOUS:
 106          // Don't bother trying to load a user for an anonymous session, since
 107          // neither the session nor the user exist.
 108          return null;
 109        case self::KIND_UNKNOWN:
 110          // If we don't know what kind of session this is, don't go looking for
 111          // it.
 112          return null;
 113        case self::KIND_USER:
 114          break;
 115        case self::KIND_EXTERNAL:
 116          // TODO: Implement these (T4310).
 117          return null;
 118      }
 119  
 120      $session_table = new PhabricatorAuthSession();
 121      $user_table = new PhabricatorUser();
 122      $conn_r = $session_table->establishConnection('r');
 123      $session_key = PhabricatorHash::digest($session_token);
 124  
 125      // NOTE: We're being clever here because this happens on every page load,
 126      // and by joining we can save a query. This might be getting too clever
 127      // for its own good, though...
 128  
 129      $info = queryfx_one(
 130        $conn_r,
 131        'SELECT
 132            s.id AS s_id,
 133            s.sessionExpires AS s_sessionExpires,
 134            s.sessionStart AS s_sessionStart,
 135            s.highSecurityUntil AS s_highSecurityUntil,
 136            s.isPartial AS s_isPartial,
 137            u.*
 138          FROM %T u JOIN %T s ON u.phid = s.userPHID
 139          AND s.type = %s AND s.sessionKey = %s',
 140        $user_table->getTableName(),
 141        $session_table->getTableName(),
 142        $session_type,
 143        $session_key);
 144  
 145      if (!$info) {
 146        return null;
 147      }
 148  
 149      $session_dict = array(
 150        'userPHID' => $info['phid'],
 151        'sessionKey' => $session_key,
 152        'type' => $session_type,
 153      );
 154      foreach ($info as $key => $value) {
 155        if (strncmp($key, 's_', 2) === 0) {
 156          unset($info[$key]);
 157          $session_dict[substr($key, 2)] = $value;
 158        }
 159      }
 160      $session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
 161  
 162      $ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
 163  
 164      // If more than 20% of the time on this session has been used, refresh the
 165      // TTL back up to the full duration. The idea here is that sessions are
 166      // good forever if used regularly, but get GC'd when they fall out of use.
 167  
 168      // NOTE: If we begin rotating session keys when extending sessions, the
 169      // CSRF code needs to be updated so CSRF tokens survive session rotation.
 170  
 171      if (time() + (0.80 * $ttl) > $session->getSessionExpires()) {
 172        $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 173          $conn_w = $session_table->establishConnection('w');
 174          queryfx(
 175            $conn_w,
 176            'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d',
 177            $session->getTableName(),
 178            $ttl,
 179            $session->getID());
 180        unset($unguarded);
 181      }
 182  
 183      $user = $user_table->loadFromArray($info);
 184      $user->attachSession($session);
 185      return $user;
 186    }
 187  
 188  
 189    /**
 190     * Issue a new session key for a given identity. Phabricator supports
 191     * different types of sessions (like "web" and "conduit") and each session
 192     * type may have multiple concurrent sessions (this allows a user to be
 193     * logged in on multiple browsers at the same time, for instance).
 194     *
 195     * Note that this method is transport-agnostic and does not set cookies or
 196     * issue other types of tokens, it ONLY generates a new session key.
 197     *
 198     * You can configure the maximum number of concurrent sessions for various
 199     * session types in the Phabricator configuration.
 200     *
 201     * @param   const     Session type constant (see
 202     *                    @{class:PhabricatorAuthSession}).
 203     * @param   phid|null Identity to establish a session for, usually a user
 204     *                    PHID. With `null`, generates an anonymous session.
 205     * @param   bool      True to issue a partial session.
 206     * @return  string    Newly generated session key.
 207     */
 208    public function establishSession($session_type, $identity_phid, $partial) {
 209      // Consume entropy to generate a new session key, forestalling the eventual
 210      // heat death of the universe.
 211      $session_key = Filesystem::readRandomCharacters(40);
 212  
 213      if ($identity_phid === null) {
 214        return self::KIND_ANONYMOUS.'/'.$session_key;
 215      }
 216  
 217      $session_table = new PhabricatorAuthSession();
 218      $conn_w = $session_table->establishConnection('w');
 219  
 220      // This has a side effect of validating the session type.
 221      $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
 222  
 223      $digest_key = PhabricatorHash::digest($session_key);
 224  
 225      // Logging-in users don't have CSRF stuff yet, so we have to unguard this
 226      // write.
 227      $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 228        id(new PhabricatorAuthSession())
 229          ->setUserPHID($identity_phid)
 230          ->setType($session_type)
 231          ->setSessionKey($digest_key)
 232          ->setSessionStart(time())
 233          ->setSessionExpires(time() + $session_ttl)
 234          ->setIsPartial($partial ? 1 : 0)
 235          ->save();
 236  
 237        $log = PhabricatorUserLog::initializeNewLog(
 238          null,
 239          $identity_phid,
 240          ($partial
 241            ? PhabricatorUserLog::ACTION_LOGIN_PARTIAL
 242            : PhabricatorUserLog::ACTION_LOGIN));
 243  
 244        $log->setDetails(
 245          array(
 246            'session_type' => $session_type,
 247          ));
 248        $log->setSession($digest_key);
 249        $log->save();
 250      unset($unguarded);
 251  
 252      return $session_key;
 253    }
 254  
 255  
 256    /**
 257     * Terminate all of a user's login sessions.
 258     *
 259     * This is used when users change passwords, linked accounts, or add
 260     * multifactor authentication.
 261     *
 262     * @param PhabricatorUser User whose sessions should be terminated.
 263     * @param string|null Optionally, one session to keep. Normally, the current
 264     *   login session.
 265     *
 266     * @return void
 267     */
 268    public function terminateLoginSessions(
 269      PhabricatorUser $user,
 270      $except_session = null) {
 271  
 272      $sessions = id(new PhabricatorAuthSessionQuery())
 273        ->setViewer($user)
 274        ->withIdentityPHIDs(array($user->getPHID()))
 275        ->execute();
 276  
 277      if ($except_session !== null) {
 278        $except_session = PhabricatorHash::digest($except_session);
 279      }
 280  
 281      foreach ($sessions as $key => $session) {
 282        if ($except_session !== null) {
 283          if ($except_session == $session->getSessionKey()) {
 284            continue;
 285          }
 286        }
 287  
 288        $session->delete();
 289      }
 290    }
 291  
 292  
 293  /* -(  High Security  )------------------------------------------------------ */
 294  
 295  
 296    /**
 297     * Require high security, or prompt the user to enter high security.
 298     *
 299     * If the user's session is in high security, this method will return a
 300     * token. Otherwise, it will throw an exception which will eventually
 301     * be converted into a multi-factor authentication workflow.
 302     *
 303     * @param PhabricatorUser User whose session needs to be in high security.
 304     * @param AphrontReqeust  Current request.
 305     * @param string          URI to return the user to if they cancel.
 306     * @param bool            True to jump partial sessions directly into high
 307     *                        security instead of just upgrading them to full
 308     *                        sessions.
 309     * @return PhabricatorAuthHighSecurityToken Security token.
 310     * @task hisec
 311     */
 312    public function requireHighSecuritySession(
 313      PhabricatorUser $viewer,
 314      AphrontRequest $request,
 315      $cancel_uri,
 316      $jump_into_hisec = false) {
 317  
 318      if (!$viewer->hasSession()) {
 319        throw new Exception(
 320          pht('Requiring a high-security session from a user with no session!'));
 321      }
 322  
 323      $session = $viewer->getSession();
 324  
 325      // Check if the session is already in high security mode.
 326      $token = $this->issueHighSecurityToken($session);
 327      if ($token) {
 328        return $token;
 329      }
 330  
 331      // Load the multi-factor auth sources attached to this account.
 332      $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
 333        'userPHID = %s',
 334        $viewer->getPHID());
 335  
 336      // If the account has no associated multi-factor auth, just issue a token
 337      // without putting the session into high security mode. This is generally
 338      // easier for users. A minor but desirable side effect is that when a user
 339      // adds an auth factor, existing sessions won't get a free pass into hisec,
 340      // since they never actually got marked as hisec.
 341      if (!$factors) {
 342        return $this->issueHighSecurityToken($session, true);
 343      }
 344  
 345      // Check for a rate limit without awarding points, so the user doesn't
 346      // get partway through the workflow only to get blocked.
 347      PhabricatorSystemActionEngine::willTakeAction(
 348        array($viewer->getPHID()),
 349        new PhabricatorAuthTryFactorAction(),
 350        0);
 351  
 352      $validation_results = array();
 353      if ($request->isHTTPPost()) {
 354        $request->validateCSRF();
 355        if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
 356  
 357          // Limit factor verification rates to prevent brute force attacks.
 358          PhabricatorSystemActionEngine::willTakeAction(
 359            array($viewer->getPHID()),
 360            new PhabricatorAuthTryFactorAction(),
 361            1);
 362  
 363          $ok = true;
 364          foreach ($factors as $factor) {
 365            $id = $factor->getID();
 366            $impl = $factor->requireImplementation();
 367  
 368            $validation_results[$id] = $impl->processValidateFactorForm(
 369              $factor,
 370              $viewer,
 371              $request);
 372  
 373            if (!$impl->isFactorValid($factor, $validation_results[$id])) {
 374              $ok = false;
 375            }
 376          }
 377  
 378          if ($ok) {
 379            // Give the user a credit back for a successful factor verification.
 380            PhabricatorSystemActionEngine::willTakeAction(
 381              array($viewer->getPHID()),
 382              new PhabricatorAuthTryFactorAction(),
 383              -1);
 384  
 385            if ($session->getIsPartial() && !$jump_into_hisec) {
 386              // If we have a partial session and are not jumping directly into
 387              // hisec, just issue a token without putting it in high security
 388              // mode.
 389              return $this->issueHighSecurityToken($session, true);
 390            }
 391  
 392            $until = time() + phutil_units('15 minutes in seconds');
 393            $session->setHighSecurityUntil($until);
 394  
 395            queryfx(
 396              $session->establishConnection('w'),
 397              'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
 398              $session->getTableName(),
 399              $until,
 400              $session->getID());
 401  
 402            $log = PhabricatorUserLog::initializeNewLog(
 403              $viewer,
 404              $viewer->getPHID(),
 405              PhabricatorUserLog::ACTION_ENTER_HISEC);
 406            $log->save();
 407          } else {
 408            $log = PhabricatorUserLog::initializeNewLog(
 409              $viewer,
 410              $viewer->getPHID(),
 411              PhabricatorUserLog::ACTION_FAIL_HISEC);
 412            $log->save();
 413          }
 414        }
 415      }
 416  
 417      $token = $this->issueHighSecurityToken($session);
 418      if ($token) {
 419        return $token;
 420      }
 421  
 422      throw id(new PhabricatorAuthHighSecurityRequiredException())
 423        ->setCancelURI($cancel_uri)
 424        ->setFactors($factors)
 425        ->setFactorValidationResults($validation_results);
 426    }
 427  
 428  
 429    /**
 430     * Issue a high security token for a session, if authorized.
 431     *
 432     * @param PhabricatorAuthSession Session to issue a token for.
 433     * @param bool Force token issue.
 434     * @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
 435     * @task hisec
 436     */
 437    private function issueHighSecurityToken(
 438      PhabricatorAuthSession $session,
 439      $force = false) {
 440  
 441      $until = $session->getHighSecurityUntil();
 442      if ($until > time() || $force) {
 443        return new PhabricatorAuthHighSecurityToken();
 444      }
 445  
 446      return null;
 447    }
 448  
 449  
 450    /**
 451     * Render a form for providing relevant multi-factor credentials.
 452     *
 453     * @param PhabricatorUser Viewing user.
 454     * @param AphrontRequest Current request.
 455     * @return AphrontFormView Renderable form.
 456     * @task hisec
 457     */
 458    public function renderHighSecurityForm(
 459      array $factors,
 460      array $validation_results,
 461      PhabricatorUser $viewer,
 462      AphrontRequest $request) {
 463  
 464      $form = id(new AphrontFormView())
 465        ->setUser($viewer)
 466        ->appendRemarkupInstructions('');
 467  
 468      foreach ($factors as $factor) {
 469        $factor->requireImplementation()->renderValidateFactorForm(
 470          $factor,
 471          $form,
 472          $viewer,
 473          idx($validation_results, $factor->getID()));
 474      }
 475  
 476      $form->appendRemarkupInstructions('');
 477  
 478      return $form;
 479    }
 480  
 481  
 482    /**
 483     * Strip the high security flag from a session.
 484     *
 485     * Kicks a session out of high security and logs the exit.
 486     *
 487     * @param PhabricatorUser Acting user.
 488     * @param PhabricatorAuthSession Session to return to normal security.
 489     * @return void
 490     * @task hisec
 491     */
 492    public function exitHighSecurity(
 493      PhabricatorUser $viewer,
 494      PhabricatorAuthSession $session) {
 495  
 496      if (!$session->getHighSecurityUntil()) {
 497        return;
 498      }
 499  
 500      queryfx(
 501        $session->establishConnection('w'),
 502        'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
 503        $session->getTableName(),
 504        $session->getID());
 505  
 506      $log = PhabricatorUserLog::initializeNewLog(
 507        $viewer,
 508        $viewer->getPHID(),
 509        PhabricatorUserLog::ACTION_EXIT_HISEC);
 510      $log->save();
 511    }
 512  
 513  
 514  /* -(  Partial Sessions  )--------------------------------------------------- */
 515  
 516  
 517    /**
 518     * Upgrade a partial session to a full session.
 519     *
 520     * @param PhabricatorAuthSession Session to upgrade.
 521     * @return void
 522     * @task partial
 523     */
 524    public function upgradePartialSession(PhabricatorUser $viewer) {
 525  
 526      if (!$viewer->hasSession()) {
 527        throw new Exception(
 528          pht('Upgrading partial session of user with no session!'));
 529      }
 530  
 531      $session = $viewer->getSession();
 532  
 533      if (!$session->getIsPartial()) {
 534        throw new Exception(pht('Session is not partial!'));
 535      }
 536  
 537      $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 538        $session->setIsPartial(0);
 539  
 540        queryfx(
 541          $session->establishConnection('w'),
 542          'UPDATE %T SET isPartial = %d WHERE id = %d',
 543          $session->getTableName(),
 544          0,
 545          $session->getID());
 546  
 547        $log = PhabricatorUserLog::initializeNewLog(
 548          $viewer,
 549          $viewer->getPHID(),
 550          PhabricatorUserLog::ACTION_LOGIN_FULL);
 551        $log->save();
 552      unset($unguarded);
 553    }
 554  
 555  
 556  /* -(  One Time Login URIs  )------------------------------------------------ */
 557  
 558  
 559    /**
 560     * Retrieve a temporary, one-time URI which can log in to an account.
 561     *
 562     * These URIs are used for password recovery and to regain access to accounts
 563     * which users have been locked out of.
 564     *
 565     * @param PhabricatorUser User to generate a URI for.
 566     * @param PhabricatorUserEmail Optionally, email to verify when
 567     *  link is used.
 568     * @param string Optional context string for the URI. This is purely cosmetic
 569     *  and used only to customize workflow and error messages.
 570     * @return string Login URI.
 571     * @task onetime
 572     */
 573    public function getOneTimeLoginURI(
 574      PhabricatorUser $user,
 575      PhabricatorUserEmail $email = null,
 576      $type = self::ONETIME_RESET) {
 577  
 578      $key = Filesystem::readRandomCharacters(32);
 579      $key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
 580  
 581      $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 582        id(new PhabricatorAuthTemporaryToken())
 583          ->setObjectPHID($user->getPHID())
 584          ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
 585          ->setTokenExpires(time() + phutil_units('1 day in seconds'))
 586          ->setTokenCode($key_hash)
 587          ->save();
 588      unset($unguarded);
 589  
 590      $uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/';
 591      if ($email) {
 592        $uri = $uri.$email->getID().'/';
 593      }
 594  
 595      try {
 596        $uri = PhabricatorEnv::getProductionURI($uri);
 597      } catch (Exception $ex) {
 598        // If a user runs `bin/auth recover` before configuring the base URI,
 599        // just show the path. We don't have any way to figure out the domain.
 600        // See T4132.
 601      }
 602  
 603      return $uri;
 604    }
 605  
 606  
 607    /**
 608     * Load the temporary token associated with a given one-time login key.
 609     *
 610     * @param PhabricatorUser User to load the token for.
 611     * @param PhabricatorUserEmail Optionally, email to verify when
 612     *  link is used.
 613     * @param string Key user is presenting as a valid one-time login key.
 614     * @return PhabricatorAuthTemporaryToken|null Token, if one exists.
 615     * @task onetime
 616     */
 617    public function loadOneTimeLoginKey(
 618      PhabricatorUser $user,
 619      PhabricatorUserEmail $email = null,
 620      $key = null) {
 621  
 622      $key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
 623  
 624      return id(new PhabricatorAuthTemporaryTokenQuery())
 625        ->setViewer($user)
 626        ->withObjectPHIDs(array($user->getPHID()))
 627        ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
 628        ->withTokenCodes(array($key_hash))
 629        ->withExpired(false)
 630        ->executeOne();
 631    }
 632  
 633  
 634    /**
 635     * Hash a one-time login key for storage as a temporary token.
 636     *
 637     * @param PhabricatorUser User this key is for.
 638     * @param PhabricatorUserEmail Optionally, email to verify when
 639     *  link is used.
 640     * @param string The one time login key.
 641     * @return string Hash of the key.
 642     * task onetime
 643     */
 644    private function getOneTimeLoginKeyHash(
 645      PhabricatorUser $user,
 646      PhabricatorUserEmail $email = null,
 647      $key = null) {
 648  
 649      $parts = array(
 650        $key,
 651        $user->getAccountSecret(),
 652      );
 653  
 654      if ($email) {
 655        $parts[] = $email->getVerificationCode();
 656      }
 657  
 658      return PhabricatorHash::digest(implode(':', $parts));
 659    }
 660  
 661  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1