[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |