Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
12256 views
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
* @task cache User Cache
11
*/
12
final class PhabricatorAuthSessionEngine extends Phobject {
13
14
/**
15
* Session issued to normal users after they login through a standard channel.
16
* Associates the client with a standard user identity.
17
*/
18
const KIND_USER = 'U';
19
20
21
/**
22
* Session issued to users who login with some sort of credentials but do not
23
* have full accounts. These are sometimes called "grey users".
24
*
25
* TODO: We do not currently issue these sessions, see T4310.
26
*/
27
const KIND_EXTERNAL = 'X';
28
29
30
/**
31
* Session issued to logged-out users which has no real identity information.
32
* Its purpose is to protect logged-out users from CSRF.
33
*/
34
const KIND_ANONYMOUS = 'A';
35
36
37
/**
38
* Session kind isn't known.
39
*/
40
const KIND_UNKNOWN = '?';
41
42
43
const ONETIME_RECOVER = 'recover';
44
const ONETIME_RESET = 'reset';
45
const ONETIME_WELCOME = 'welcome';
46
const ONETIME_USERNAME = 'rename';
47
48
49
private $workflowKey;
50
private $request;
51
52
public function setWorkflowKey($workflow_key) {
53
$this->workflowKey = $workflow_key;
54
return $this;
55
}
56
57
public function getWorkflowKey() {
58
59
// TODO: A workflow key should become required in order to issue an MFA
60
// challenge, but allow things to keep working for now until we can update
61
// callsites.
62
if ($this->workflowKey === null) {
63
return 'legacy';
64
}
65
66
return $this->workflowKey;
67
}
68
69
public function getRequest() {
70
return $this->request;
71
}
72
73
74
/**
75
* Get the session kind (e.g., anonymous, user, external account) from a
76
* session token. Returns a `KIND_` constant.
77
*
78
* @param string Session token.
79
* @return const Session kind constant.
80
*/
81
public static function getSessionKindFromToken($session_token) {
82
if (strpos($session_token, '/') === false) {
83
// Old-style session, these are all user sessions.
84
return self::KIND_USER;
85
}
86
87
list($kind, $key) = explode('/', $session_token, 2);
88
89
switch ($kind) {
90
case self::KIND_ANONYMOUS:
91
case self::KIND_USER:
92
case self::KIND_EXTERNAL:
93
return $kind;
94
default:
95
return self::KIND_UNKNOWN;
96
}
97
}
98
99
100
/**
101
* Load the user identity associated with a session of a given type,
102
* identified by token.
103
*
104
* When the user presents a session token to an API, this method verifies
105
* it is of the correct type and loads the corresponding identity if the
106
* session exists and is valid.
107
*
108
* NOTE: `$session_type` is the type of session that is required by the
109
* loading context. This prevents use of a Conduit sesssion as a Web
110
* session, for example.
111
*
112
* @param const The type of session to load.
113
* @param string The session token.
114
* @return PhabricatorUser|null
115
* @task use
116
*/
117
public function loadUserForSession($session_type, $session_token) {
118
$session_kind = self::getSessionKindFromToken($session_token);
119
switch ($session_kind) {
120
case self::KIND_ANONYMOUS:
121
// Don't bother trying to load a user for an anonymous session, since
122
// neither the session nor the user exist.
123
return null;
124
case self::KIND_UNKNOWN:
125
// If we don't know what kind of session this is, don't go looking for
126
// it.
127
return null;
128
case self::KIND_USER:
129
break;
130
case self::KIND_EXTERNAL:
131
// TODO: Implement these (T4310).
132
return null;
133
}
134
135
$session_table = new PhabricatorAuthSession();
136
$user_table = new PhabricatorUser();
137
$conn = $session_table->establishConnection('r');
138
139
// TODO: See T13225. We're moving sessions to a more modern digest
140
// algorithm, but still accept older cookies for compatibility.
141
$session_key = PhabricatorAuthSession::newSessionDigest(
142
new PhutilOpaqueEnvelope($session_token));
143
$weak_key = PhabricatorHash::weakDigest($session_token);
144
145
$cache_parts = $this->getUserCacheQueryParts($conn);
146
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
147
148
$info = queryfx_one(
149
$conn,
150
'SELECT
151
s.id AS s_id,
152
s.phid AS s_phid,
153
s.sessionExpires AS s_sessionExpires,
154
s.sessionStart AS s_sessionStart,
155
s.highSecurityUntil AS s_highSecurityUntil,
156
s.isPartial AS s_isPartial,
157
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
158
IF(s.sessionKey = %P, 1, 0) as s_weak,
159
u.*
160
%Q
161
FROM %R u JOIN %R s ON u.phid = s.userPHID
162
AND s.type = %s AND s.sessionKey IN (%P, %P) %Q',
163
new PhutilOpaqueEnvelope($weak_key),
164
$cache_selects,
165
$user_table,
166
$session_table,
167
$session_type,
168
new PhutilOpaqueEnvelope($session_key),
169
new PhutilOpaqueEnvelope($weak_key),
170
$cache_joins);
171
172
if (!$info) {
173
return null;
174
}
175
176
// TODO: Remove this, see T13225.
177
$is_weak = (bool)$info['s_weak'];
178
unset($info['s_weak']);
179
180
$session_dict = array(
181
'userPHID' => $info['phid'],
182
'sessionKey' => $session_key,
183
'type' => $session_type,
184
);
185
186
$cache_raw = array_fill_keys($cache_map, null);
187
foreach ($info as $key => $value) {
188
if (strncmp($key, 's_', 2) === 0) {
189
unset($info[$key]);
190
$session_dict[substr($key, 2)] = $value;
191
continue;
192
}
193
194
if (isset($cache_map[$key])) {
195
unset($info[$key]);
196
$cache_raw[$cache_map[$key]] = $value;
197
continue;
198
}
199
}
200
201
$user = $user_table->loadFromArray($info);
202
203
$cache_raw = $this->filterRawCacheData($user, $types_map, $cache_raw);
204
$user->attachRawCacheData($cache_raw);
205
206
switch ($session_type) {
207
case PhabricatorAuthSession::TYPE_WEB:
208
// Explicitly prevent bots and mailing lists from establishing web
209
// sessions. It's normally impossible to attach authentication to these
210
// accounts, and likewise impossible to generate sessions, but it's
211
// technically possible that a session could exist in the database. If
212
// one does somehow, refuse to load it.
213
if (!$user->canEstablishWebSessions()) {
214
return null;
215
}
216
break;
217
}
218
219
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
220
221
$this->extendSession($session);
222
223
// TODO: Remove this, see T13225.
224
if ($is_weak) {
225
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
226
$conn_w = $session_table->establishConnection('w');
227
queryfx(
228
$conn_w,
229
'UPDATE %T SET sessionKey = %P WHERE id = %d',
230
$session->getTableName(),
231
new PhutilOpaqueEnvelope($session_key),
232
$session->getID());
233
unset($unguarded);
234
}
235
236
$user->attachSession($session);
237
return $user;
238
}
239
240
241
/**
242
* Issue a new session key for a given identity. Phabricator supports
243
* different types of sessions (like "web" and "conduit") and each session
244
* type may have multiple concurrent sessions (this allows a user to be
245
* logged in on multiple browsers at the same time, for instance).
246
*
247
* Note that this method is transport-agnostic and does not set cookies or
248
* issue other types of tokens, it ONLY generates a new session key.
249
*
250
* You can configure the maximum number of concurrent sessions for various
251
* session types in the Phabricator configuration.
252
*
253
* @param const Session type constant (see
254
* @{class:PhabricatorAuthSession}).
255
* @param phid|null Identity to establish a session for, usually a user
256
* PHID. With `null`, generates an anonymous session.
257
* @param bool True to issue a partial session.
258
* @return string Newly generated session key.
259
*/
260
public function establishSession($session_type, $identity_phid, $partial) {
261
// Consume entropy to generate a new session key, forestalling the eventual
262
// heat death of the universe.
263
$session_key = Filesystem::readRandomCharacters(40);
264
265
if ($identity_phid === null) {
266
return self::KIND_ANONYMOUS.'/'.$session_key;
267
}
268
269
$session_table = new PhabricatorAuthSession();
270
$conn_w = $session_table->establishConnection('w');
271
272
// This has a side effect of validating the session type.
273
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL(
274
$session_type,
275
$partial);
276
277
$digest_key = PhabricatorAuthSession::newSessionDigest(
278
new PhutilOpaqueEnvelope($session_key));
279
280
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
281
// write.
282
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
283
id(new PhabricatorAuthSession())
284
->setUserPHID($identity_phid)
285
->setType($session_type)
286
->setSessionKey($digest_key)
287
->setSessionStart(time())
288
->setSessionExpires(time() + $session_ttl)
289
->setIsPartial($partial ? 1 : 0)
290
->setSignedLegalpadDocuments(0)
291
->save();
292
293
$log = PhabricatorUserLog::initializeNewLog(
294
null,
295
$identity_phid,
296
($partial
297
? PhabricatorPartialLoginUserLogType::LOGTYPE
298
: PhabricatorLoginUserLogType::LOGTYPE));
299
300
$log->setDetails(
301
array(
302
'session_type' => $session_type,
303
));
304
$log->setSession($digest_key);
305
$log->save();
306
unset($unguarded);
307
308
$info = id(new PhabricatorAuthSessionInfo())
309
->setSessionType($session_type)
310
->setIdentityPHID($identity_phid)
311
->setIsPartial($partial);
312
313
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
314
foreach ($extensions as $extension) {
315
$extension->didEstablishSession($info);
316
}
317
318
return $session_key;
319
}
320
321
322
/**
323
* Terminate all of a user's login sessions.
324
*
325
* This is used when users change passwords, linked accounts, or add
326
* multifactor authentication.
327
*
328
* @param PhabricatorUser User whose sessions should be terminated.
329
* @param string|null Optionally, one session to keep. Normally, the current
330
* login session.
331
*
332
* @return void
333
*/
334
public function terminateLoginSessions(
335
PhabricatorUser $user,
336
PhutilOpaqueEnvelope $except_session = null) {
337
338
$sessions = id(new PhabricatorAuthSessionQuery())
339
->setViewer($user)
340
->withIdentityPHIDs(array($user->getPHID()))
341
->execute();
342
343
if ($except_session !== null) {
344
$except_session = PhabricatorAuthSession::newSessionDigest(
345
$except_session);
346
}
347
348
foreach ($sessions as $key => $session) {
349
if ($except_session !== null) {
350
$is_except = phutil_hashes_are_identical(
351
$session->getSessionKey(),
352
$except_session);
353
if ($is_except) {
354
continue;
355
}
356
}
357
358
$session->delete();
359
}
360
}
361
362
public function logoutSession(
363
PhabricatorUser $user,
364
PhabricatorAuthSession $session) {
365
366
$log = PhabricatorUserLog::initializeNewLog(
367
$user,
368
$user->getPHID(),
369
PhabricatorLogoutUserLogType::LOGTYPE);
370
$log->save();
371
372
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
373
foreach ($extensions as $extension) {
374
$extension->didLogout($user, array($session));
375
}
376
377
$session->delete();
378
}
379
380
381
/* -( High Security )------------------------------------------------------ */
382
383
384
/**
385
* Require the user respond to a high security (MFA) check.
386
*
387
* This method differs from @{method:requireHighSecuritySession} in that it
388
* does not upgrade the user's session as a side effect. This method is
389
* appropriate for one-time checks.
390
*
391
* @param PhabricatorUser User whose session needs to be in high security.
392
* @param AphrontRequest Current request.
393
* @param string URI to return the user to if they cancel.
394
* @return PhabricatorAuthHighSecurityToken Security token.
395
* @task hisec
396
*/
397
public function requireHighSecurityToken(
398
PhabricatorUser $viewer,
399
AphrontRequest $request,
400
$cancel_uri) {
401
402
return $this->newHighSecurityToken(
403
$viewer,
404
$request,
405
$cancel_uri,
406
false,
407
false);
408
}
409
410
411
/**
412
* Require high security, or prompt the user to enter high security.
413
*
414
* If the user's session is in high security, this method will return a
415
* token. Otherwise, it will throw an exception which will eventually
416
* be converted into a multi-factor authentication workflow.
417
*
418
* This method upgrades the user's session to high security for a short
419
* period of time, and is appropriate if you anticipate they may need to
420
* take multiple high security actions. To perform a one-time check instead,
421
* use @{method:requireHighSecurityToken}.
422
*
423
* @param PhabricatorUser User whose session needs to be in high security.
424
* @param AphrontRequest Current request.
425
* @param string URI to return the user to if they cancel.
426
* @param bool True to jump partial sessions directly into high
427
* security instead of just upgrading them to full
428
* sessions.
429
* @return PhabricatorAuthHighSecurityToken Security token.
430
* @task hisec
431
*/
432
public function requireHighSecuritySession(
433
PhabricatorUser $viewer,
434
AphrontRequest $request,
435
$cancel_uri,
436
$jump_into_hisec = false) {
437
438
return $this->newHighSecurityToken(
439
$viewer,
440
$request,
441
$cancel_uri,
442
$jump_into_hisec,
443
true);
444
}
445
446
private function newHighSecurityToken(
447
PhabricatorUser $viewer,
448
AphrontRequest $request,
449
$cancel_uri,
450
$jump_into_hisec,
451
$upgrade_session) {
452
453
if (!$viewer->hasSession()) {
454
throw new Exception(
455
pht('Requiring a high-security session from a user with no session!'));
456
}
457
458
// TODO: If a user answers a "requireHighSecurityToken()" prompt and hits
459
// a "requireHighSecuritySession()" prompt a short time later, the one-shot
460
// token should be good enough to upgrade the session.
461
462
$session = $viewer->getSession();
463
464
// Check if the session is already in high security mode.
465
$token = $this->issueHighSecurityToken($session);
466
if ($token) {
467
return $token;
468
}
469
470
// Load the multi-factor auth sources attached to this account. Note that
471
// we order factors from oldest to newest, which is not the default query
472
// ordering but makes the greatest sense in context.
473
$factors = id(new PhabricatorAuthFactorConfigQuery())
474
->setViewer($viewer)
475
->withUserPHIDs(array($viewer->getPHID()))
476
->withFactorProviderStatuses(
477
array(
478
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
479
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
480
))
481
->execute();
482
483
// Sort factors in the same order that they appear in on the Settings
484
// panel. This means that administrators changing provider statuses may
485
// change the order of prompts for users, but the alternative is that the
486
// Settings panel order disagrees with the prompt order, which seems more
487
// disruptive.
488
$factors = msortv($factors, 'newSortVector');
489
490
// If the account has no associated multi-factor auth, just issue a token
491
// without putting the session into high security mode. This is generally
492
// easier for users. A minor but desirable side effect is that when a user
493
// adds an auth factor, existing sessions won't get a free pass into hisec,
494
// since they never actually got marked as hisec.
495
if (!$factors) {
496
return $this->issueHighSecurityToken($session, true)
497
->setIsUnchallengedToken(true);
498
}
499
500
$this->request = $request;
501
foreach ($factors as $factor) {
502
$factor->setSessionEngine($this);
503
}
504
505
// Check for a rate limit without awarding points, so the user doesn't
506
// get partway through the workflow only to get blocked.
507
PhabricatorSystemActionEngine::willTakeAction(
508
array($viewer->getPHID()),
509
new PhabricatorAuthTryFactorAction(),
510
0);
511
512
$now = PhabricatorTime::getNow();
513
514
// We need to do challenge validation first, since this happens whether you
515
// submitted responses or not. You can't get a "bad response" error before
516
// you actually submit a response, but you can get a "wait, we can't
517
// issue a challenge yet" response. Load all issued challenges which are
518
// currently valid.
519
$challenges = id(new PhabricatorAuthChallengeQuery())
520
->setViewer($viewer)
521
->withFactorPHIDs(mpull($factors, 'getPHID'))
522
->withUserPHIDs(array($viewer->getPHID()))
523
->withChallengeTTLBetween($now, null)
524
->execute();
525
526
PhabricatorAuthChallenge::newChallengeResponsesFromRequest(
527
$challenges,
528
$request);
529
530
$challenge_map = mgroup($challenges, 'getFactorPHID');
531
532
$validation_results = array();
533
$ok = true;
534
535
// Validate each factor against issued challenges. For example, this
536
// prevents you from receiving or responding to a TOTP challenge if another
537
// challenge was recently issued to a different session.
538
foreach ($factors as $factor) {
539
$factor_phid = $factor->getPHID();
540
$issued_challenges = idx($challenge_map, $factor_phid, array());
541
$provider = $factor->getFactorProvider();
542
$impl = $provider->getFactor();
543
544
$new_challenges = $impl->getNewIssuedChallenges(
545
$factor,
546
$viewer,
547
$issued_challenges);
548
549
// NOTE: We may get a list of challenges back, or may just get an early
550
// result. For example, this can happen on an SMS factor if all SMS
551
// mailers have been disabled.
552
if ($new_challenges instanceof PhabricatorAuthFactorResult) {
553
$result = $new_challenges;
554
555
if (!$result->getIsValid()) {
556
$ok = false;
557
}
558
559
$validation_results[$factor_phid] = $result;
560
$challenge_map[$factor_phid] = $issued_challenges;
561
continue;
562
}
563
564
foreach ($new_challenges as $new_challenge) {
565
$issued_challenges[] = $new_challenge;
566
}
567
$challenge_map[$factor_phid] = $issued_challenges;
568
569
if (!$issued_challenges) {
570
continue;
571
}
572
573
$result = $impl->getResultFromIssuedChallenges(
574
$factor,
575
$viewer,
576
$issued_challenges);
577
578
if (!$result) {
579
continue;
580
}
581
582
if (!$result->getIsValid()) {
583
$ok = false;
584
}
585
586
$validation_results[$factor_phid] = $result;
587
}
588
589
if ($request->isHTTPPost()) {
590
$request->validateCSRF();
591
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
592
593
// Limit factor verification rates to prevent brute force attacks.
594
$any_attempt = false;
595
foreach ($factors as $factor) {
596
$factor_phid = $factor->getPHID();
597
598
$provider = $factor->getFactorProvider();
599
$impl = $provider->getFactor();
600
601
// If we already have a result (normally "wait..."), we won't try
602
// to validate whatever the user submitted, so this doesn't count as
603
// an attempt for rate limiting purposes.
604
if (isset($validation_results[$factor_phid])) {
605
continue;
606
}
607
608
if ($impl->getRequestHasChallengeResponse($factor, $request)) {
609
$any_attempt = true;
610
break;
611
}
612
}
613
614
if ($any_attempt) {
615
PhabricatorSystemActionEngine::willTakeAction(
616
array($viewer->getPHID()),
617
new PhabricatorAuthTryFactorAction(),
618
1);
619
}
620
621
foreach ($factors as $factor) {
622
$factor_phid = $factor->getPHID();
623
624
// If we already have a validation result from previously issued
625
// challenges, skip validating this factor.
626
if (isset($validation_results[$factor_phid])) {
627
continue;
628
}
629
630
$issued_challenges = idx($challenge_map, $factor_phid, array());
631
632
$provider = $factor->getFactorProvider();
633
$impl = $provider->getFactor();
634
635
$validation_result = $impl->getResultFromChallengeResponse(
636
$factor,
637
$viewer,
638
$request,
639
$issued_challenges);
640
641
if (!$validation_result->getIsValid()) {
642
$ok = false;
643
}
644
645
$validation_results[$factor_phid] = $validation_result;
646
}
647
648
if ($ok) {
649
// We're letting you through, so mark all the challenges you
650
// responded to as completed. These challenges can never be used
651
// again, even by the same session and workflow: you can't use the
652
// same response to take two different actions, even if those actions
653
// are of the same type.
654
foreach ($validation_results as $validation_result) {
655
$challenge = $validation_result->getAnsweredChallenge()
656
->markChallengeAsCompleted();
657
}
658
659
// Give the user a credit back for a successful factor verification.
660
if ($any_attempt) {
661
PhabricatorSystemActionEngine::willTakeAction(
662
array($viewer->getPHID()),
663
new PhabricatorAuthTryFactorAction(),
664
-1);
665
}
666
667
if ($session->getIsPartial() && !$jump_into_hisec) {
668
// If we have a partial session and are not jumping directly into
669
// hisec, just issue a token without putting it in high security
670
// mode.
671
return $this->issueHighSecurityToken($session, true);
672
}
673
674
// If we aren't upgrading the session itself, just issue a token.
675
if (!$upgrade_session) {
676
return $this->issueHighSecurityToken($session, true);
677
}
678
679
$until = time() + phutil_units('15 minutes in seconds');
680
$session->setHighSecurityUntil($until);
681
682
queryfx(
683
$session->establishConnection('w'),
684
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
685
$session->getTableName(),
686
$until,
687
$session->getID());
688
689
$log = PhabricatorUserLog::initializeNewLog(
690
$viewer,
691
$viewer->getPHID(),
692
PhabricatorEnterHisecUserLogType::LOGTYPE);
693
$log->save();
694
} else {
695
$log = PhabricatorUserLog::initializeNewLog(
696
$viewer,
697
$viewer->getPHID(),
698
PhabricatorFailHisecUserLogType::LOGTYPE);
699
$log->save();
700
}
701
}
702
}
703
704
$token = $this->issueHighSecurityToken($session);
705
if ($token) {
706
return $token;
707
}
708
709
// If we don't have a validation result for some factors yet, fill them
710
// in with an empty result so form rendering doesn't have to care if the
711
// results exist or not. This happens when you first load the form and have
712
// not submitted any responses yet.
713
foreach ($factors as $factor) {
714
$factor_phid = $factor->getPHID();
715
if (isset($validation_results[$factor_phid])) {
716
continue;
717
}
718
719
$issued_challenges = idx($challenge_map, $factor_phid, array());
720
721
$validation_results[$factor_phid] = $impl->getResultForPrompt(
722
$factor,
723
$viewer,
724
$request,
725
$issued_challenges);
726
}
727
728
throw id(new PhabricatorAuthHighSecurityRequiredException())
729
->setCancelURI($cancel_uri)
730
->setIsSessionUpgrade($upgrade_session)
731
->setFactors($factors)
732
->setFactorValidationResults($validation_results);
733
}
734
735
736
/**
737
* Issue a high security token for a session, if authorized.
738
*
739
* @param PhabricatorAuthSession Session to issue a token for.
740
* @param bool Force token issue.
741
* @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
742
* @task hisec
743
*/
744
private function issueHighSecurityToken(
745
PhabricatorAuthSession $session,
746
$force = false) {
747
748
if ($session->isHighSecuritySession() || $force) {
749
return new PhabricatorAuthHighSecurityToken();
750
}
751
752
return null;
753
}
754
755
756
/**
757
* Render a form for providing relevant multi-factor credentials.
758
*
759
* @param PhabricatorUser Viewing user.
760
* @param AphrontRequest Current request.
761
* @return AphrontFormView Renderable form.
762
* @task hisec
763
*/
764
public function renderHighSecurityForm(
765
array $factors,
766
array $validation_results,
767
PhabricatorUser $viewer,
768
AphrontRequest $request) {
769
assert_instances_of($validation_results, 'PhabricatorAuthFactorResult');
770
771
$form = id(new AphrontFormView())
772
->setUser($viewer)
773
->appendRemarkupInstructions('');
774
775
$answered = array();
776
foreach ($factors as $factor) {
777
$result = $validation_results[$factor->getPHID()];
778
779
$provider = $factor->getFactorProvider();
780
$impl = $provider->getFactor();
781
782
$impl->renderValidateFactorForm(
783
$factor,
784
$form,
785
$viewer,
786
$result);
787
788
$answered_challenge = $result->getAnsweredChallenge();
789
if ($answered_challenge) {
790
$answered[] = $answered_challenge;
791
}
792
}
793
794
$form->appendRemarkupInstructions('');
795
796
if ($answered) {
797
$http_params = PhabricatorAuthChallenge::newHTTPParametersFromChallenges(
798
$answered);
799
foreach ($http_params as $key => $value) {
800
$form->addHiddenInput($key, $value);
801
}
802
}
803
804
return $form;
805
}
806
807
808
/**
809
* Strip the high security flag from a session.
810
*
811
* Kicks a session out of high security and logs the exit.
812
*
813
* @param PhabricatorUser Acting user.
814
* @param PhabricatorAuthSession Session to return to normal security.
815
* @return void
816
* @task hisec
817
*/
818
public function exitHighSecurity(
819
PhabricatorUser $viewer,
820
PhabricatorAuthSession $session) {
821
822
if (!$session->getHighSecurityUntil()) {
823
return;
824
}
825
826
queryfx(
827
$session->establishConnection('w'),
828
'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
829
$session->getTableName(),
830
$session->getID());
831
832
$log = PhabricatorUserLog::initializeNewLog(
833
$viewer,
834
$viewer->getPHID(),
835
PhabricatorExitHisecUserLogType::LOGTYPE);
836
$log->save();
837
}
838
839
840
/* -( Partial Sessions )--------------------------------------------------- */
841
842
843
/**
844
* Upgrade a partial session to a full session.
845
*
846
* @param PhabricatorAuthSession Session to upgrade.
847
* @return void
848
* @task partial
849
*/
850
public function upgradePartialSession(PhabricatorUser $viewer) {
851
852
if (!$viewer->hasSession()) {
853
throw new Exception(
854
pht('Upgrading partial session of user with no session!'));
855
}
856
857
$session = $viewer->getSession();
858
859
if (!$session->getIsPartial()) {
860
throw new Exception(pht('Session is not partial!'));
861
}
862
863
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
864
$session->setIsPartial(0);
865
866
queryfx(
867
$session->establishConnection('w'),
868
'UPDATE %T SET isPartial = %d WHERE id = %d',
869
$session->getTableName(),
870
0,
871
$session->getID());
872
873
$log = PhabricatorUserLog::initializeNewLog(
874
$viewer,
875
$viewer->getPHID(),
876
PhabricatorFullLoginUserLogType::LOGTYPE);
877
$log->save();
878
unset($unguarded);
879
}
880
881
882
/* -( Legalpad Documents )-------------------------------------------------- */
883
884
885
/**
886
* Upgrade a session to have all legalpad documents signed.
887
*
888
* @param PhabricatorUser User whose session should upgrade.
889
* @param array LegalpadDocument objects
890
* @return void
891
* @task partial
892
*/
893
public function signLegalpadDocuments(PhabricatorUser $viewer, array $docs) {
894
895
if (!$viewer->hasSession()) {
896
throw new Exception(
897
pht('Signing session legalpad documents of user with no session!'));
898
}
899
900
$session = $viewer->getSession();
901
902
if ($session->getSignedLegalpadDocuments()) {
903
throw new Exception(pht(
904
'Session has already signed required legalpad documents!'));
905
}
906
907
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
908
$session->setSignedLegalpadDocuments(1);
909
910
queryfx(
911
$session->establishConnection('w'),
912
'UPDATE %T SET signedLegalpadDocuments = %d WHERE id = %d',
913
$session->getTableName(),
914
1,
915
$session->getID());
916
917
if (!empty($docs)) {
918
$log = PhabricatorUserLog::initializeNewLog(
919
$viewer,
920
$viewer->getPHID(),
921
PhabricatorSignDocumentsUserLogType::LOGTYPE);
922
$log->save();
923
}
924
unset($unguarded);
925
}
926
927
928
/* -( One Time Login URIs )------------------------------------------------ */
929
930
931
/**
932
* Retrieve a temporary, one-time URI which can log in to an account.
933
*
934
* These URIs are used for password recovery and to regain access to accounts
935
* which users have been locked out of.
936
*
937
* @param PhabricatorUser User to generate a URI for.
938
* @param PhabricatorUserEmail Optionally, email to verify when
939
* link is used.
940
* @param string Optional context string for the URI. This is purely cosmetic
941
* and used only to customize workflow and error messages.
942
* @param bool True to generate a URI which forces an immediate upgrade to
943
* a full session, bypassing MFA and other login checks.
944
* @return string Login URI.
945
* @task onetime
946
*/
947
public function getOneTimeLoginURI(
948
PhabricatorUser $user,
949
PhabricatorUserEmail $email = null,
950
$type = self::ONETIME_RESET,
951
$force_full_session = false) {
952
953
$key = Filesystem::readRandomCharacters(32);
954
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
955
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
956
957
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
958
$token = id(new PhabricatorAuthTemporaryToken())
959
->setTokenResource($user->getPHID())
960
->setTokenType($onetime_type)
961
->setTokenExpires(time() + phutil_units('1 day in seconds'))
962
->setTokenCode($key_hash)
963
->setShouldForceFullSession($force_full_session)
964
->save();
965
unset($unguarded);
966
967
$uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/';
968
if ($email) {
969
$uri = $uri.$email->getID().'/';
970
}
971
972
try {
973
$uri = PhabricatorEnv::getProductionURI($uri);
974
} catch (Exception $ex) {
975
// If a user runs `bin/auth recover` before configuring the base URI,
976
// just show the path. We don't have any way to figure out the domain.
977
// See T4132.
978
}
979
980
return $uri;
981
}
982
983
984
/**
985
* Load the temporary token associated with a given one-time login key.
986
*
987
* @param PhabricatorUser User to load the token for.
988
* @param PhabricatorUserEmail Optionally, email to verify when
989
* link is used.
990
* @param string Key user is presenting as a valid one-time login key.
991
* @return PhabricatorAuthTemporaryToken|null Token, if one exists.
992
* @task onetime
993
*/
994
public function loadOneTimeLoginKey(
995
PhabricatorUser $user,
996
PhabricatorUserEmail $email = null,
997
$key = null) {
998
999
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
1000
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
1001
1002
return id(new PhabricatorAuthTemporaryTokenQuery())
1003
->setViewer($user)
1004
->withTokenResources(array($user->getPHID()))
1005
->withTokenTypes(array($onetime_type))
1006
->withTokenCodes(array($key_hash))
1007
->withExpired(false)
1008
->executeOne();
1009
}
1010
1011
1012
/**
1013
* Hash a one-time login key for storage as a temporary token.
1014
*
1015
* @param PhabricatorUser User this key is for.
1016
* @param PhabricatorUserEmail Optionally, email to verify when
1017
* link is used.
1018
* @param string The one time login key.
1019
* @return string Hash of the key.
1020
* task onetime
1021
*/
1022
private function getOneTimeLoginKeyHash(
1023
PhabricatorUser $user,
1024
PhabricatorUserEmail $email = null,
1025
$key = null) {
1026
1027
$parts = array(
1028
$key,
1029
$user->getAccountSecret(),
1030
);
1031
1032
if ($email) {
1033
$parts[] = $email->getVerificationCode();
1034
}
1035
1036
return PhabricatorHash::weakDigest(implode(':', $parts));
1037
}
1038
1039
1040
/* -( User Cache )--------------------------------------------------------- */
1041
1042
1043
/**
1044
* @task cache
1045
*/
1046
private function getUserCacheQueryParts(AphrontDatabaseConnection $conn) {
1047
$cache_selects = array();
1048
$cache_joins = array();
1049
$cache_map = array();
1050
1051
$keys = array();
1052
$types_map = array();
1053
1054
$cache_types = PhabricatorUserCacheType::getAllCacheTypes();
1055
foreach ($cache_types as $cache_type) {
1056
foreach ($cache_type->getAutoloadKeys() as $autoload_key) {
1057
$keys[] = $autoload_key;
1058
$types_map[$autoload_key] = $cache_type;
1059
}
1060
}
1061
1062
$cache_table = id(new PhabricatorUserCache())->getTableName();
1063
1064
$cache_idx = 1;
1065
foreach ($keys as $key) {
1066
$join_as = 'ucache_'.$cache_idx;
1067
$select_as = 'ucache_'.$cache_idx.'_v';
1068
1069
$cache_selects[] = qsprintf(
1070
$conn,
1071
'%T.cacheData %T',
1072
$join_as,
1073
$select_as);
1074
1075
$cache_joins[] = qsprintf(
1076
$conn,
1077
'LEFT JOIN %T AS %T ON u.phid = %T.userPHID
1078
AND %T.cacheIndex = %s',
1079
$cache_table,
1080
$join_as,
1081
$join_as,
1082
$join_as,
1083
PhabricatorHash::digestForIndex($key));
1084
1085
$cache_map[$select_as] = $key;
1086
1087
$cache_idx++;
1088
}
1089
1090
if ($cache_selects) {
1091
$cache_selects = qsprintf($conn, ', %LQ', $cache_selects);
1092
} else {
1093
$cache_selects = qsprintf($conn, '');
1094
}
1095
1096
if ($cache_joins) {
1097
$cache_joins = qsprintf($conn, '%LJ', $cache_joins);
1098
} else {
1099
$cache_joins = qsprintf($conn, '');
1100
}
1101
1102
return array($cache_selects, $cache_joins, $cache_map, $types_map);
1103
}
1104
1105
private function filterRawCacheData(
1106
PhabricatorUser $user,
1107
array $types_map,
1108
array $cache_raw) {
1109
1110
foreach ($cache_raw as $cache_key => $cache_data) {
1111
$type = $types_map[$cache_key];
1112
if ($type->shouldValidateRawCacheData()) {
1113
if (!$type->isRawCacheDataValid($user, $cache_key, $cache_data)) {
1114
unset($cache_raw[$cache_key]);
1115
}
1116
}
1117
}
1118
1119
return $cache_raw;
1120
}
1121
1122
public function willServeRequestForUser(PhabricatorUser $user) {
1123
// We allow the login user to generate any missing cache data inline.
1124
$user->setAllowInlineCacheGeneration(true);
1125
1126
// Switch to the user's translation.
1127
PhabricatorEnv::setLocaleCode($user->getTranslation());
1128
1129
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
1130
foreach ($extensions as $extension) {
1131
$extension->willServeRequestForUser($user);
1132
}
1133
}
1134
1135
private function extendSession(PhabricatorAuthSession $session) {
1136
$is_partial = $session->getIsPartial();
1137
1138
// Don't extend partial sessions. You have a relatively short window to
1139
// upgrade into a full session, and your session expires otherwise.
1140
if ($is_partial) {
1141
return;
1142
}
1143
1144
$session_type = $session->getType();
1145
1146
$ttl = PhabricatorAuthSession::getSessionTypeTTL(
1147
$session_type,
1148
$session->getIsPartial());
1149
1150
// If more than 20% of the time on this session has been used, refresh the
1151
// TTL back up to the full duration. The idea here is that sessions are
1152
// good forever if used regularly, but get GC'd when they fall out of use.
1153
1154
$now = PhabricatorTime::getNow();
1155
if ($now + (0.80 * $ttl) <= $session->getSessionExpires()) {
1156
return;
1157
}
1158
1159
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
1160
queryfx(
1161
$session->establishConnection('w'),
1162
'UPDATE %R SET sessionExpires = UNIX_TIMESTAMP() + %d
1163
WHERE id = %d',
1164
$session,
1165
$ttl,
1166
$session->getID());
1167
unset($unguarded);
1168
}
1169
1170
1171
}
1172
1173