Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
13401 views
1
<?php
2
3
final class PhabricatorAuthOneTimeLoginController
4
extends PhabricatorAuthController {
5
6
public function shouldRequireLogin() {
7
return false;
8
}
9
10
public function handleRequest(AphrontRequest $request) {
11
$viewer = $this->getViewer();
12
$id = $request->getURIData('id');
13
$link_type = $request->getURIData('type');
14
$key = $request->getURIData('key');
15
$email_id = $request->getURIData('emailID');
16
17
$target_user = id(new PhabricatorPeopleQuery())
18
->setViewer(PhabricatorUser::getOmnipotentUser())
19
->withIDs(array($id))
20
->executeOne();
21
if (!$target_user) {
22
return new Aphront404Response();
23
}
24
25
// NOTE: We allow you to use a one-time login link for your own current
26
// login account. This supports the "Set Password" flow.
27
28
$is_logged_in = false;
29
if ($viewer->isLoggedIn()) {
30
if ($viewer->getPHID() !== $target_user->getPHID()) {
31
return $this->renderError(
32
pht('You are already logged in.'));
33
} else {
34
$is_logged_in = true;
35
}
36
}
37
38
// NOTE: As a convenience to users, these one-time login URIs may also
39
// be associated with an email address which will be verified when the
40
// URI is used.
41
42
// This improves the new user experience for users receiving "Welcome"
43
// emails on installs that require verification: if we did not verify the
44
// email, they'd immediately get roadblocked with a "Verify Your Email"
45
// error and have to go back to their email account, wait for a
46
// "Verification" email, and then click that link to actually get access to
47
// their account. This is hugely unwieldy, and if the link was only sent
48
// to the user's email in the first place we can safely verify it as a
49
// side effect of login.
50
51
// The email hashed into the URI so users can't verify some email they
52
// do not own by doing this:
53
//
54
// - Add some address you do not own;
55
// - request a password reset;
56
// - change the URI in the email to the address you don't own;
57
// - login via the email link; and
58
// - get a "verified" address you don't control.
59
60
$target_email = null;
61
if ($email_id) {
62
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
63
'userPHID = %s AND id = %d',
64
$target_user->getPHID(),
65
$email_id);
66
if (!$target_email) {
67
return new Aphront404Response();
68
}
69
}
70
71
$engine = new PhabricatorAuthSessionEngine();
72
$token = $engine->loadOneTimeLoginKey(
73
$target_user,
74
$target_email,
75
$key);
76
77
if (!$token) {
78
return $this->newDialog()
79
->setTitle(pht('Unable to Log In'))
80
->setShortTitle(pht('Login Failure'))
81
->appendParagraph(
82
pht(
83
'The login link you clicked is invalid, out of date, or has '.
84
'already been used.'))
85
->appendParagraph(
86
pht(
87
'Make sure you are copy-and-pasting the entire link into '.
88
'your browser. Login links are only valid for 24 hours, and '.
89
'can only be used once.'))
90
->appendParagraph(
91
pht('You can try again, or request a new link via email.'))
92
->addCancelButton('/login/email/', pht('Send Another Email'));
93
}
94
95
if (!$target_user->canEstablishWebSessions()) {
96
return $this->newDialog()
97
->setTitle(pht('Unable to Establish Web Session'))
98
->setShortTitle(pht('Login Failure'))
99
->appendParagraph(
100
pht(
101
'You are trying to gain access to an account ("%s") that can not '.
102
'establish a web session.',
103
$target_user->getUsername()))
104
->appendParagraph(
105
pht(
106
'Special users like daemons and mailing lists are not permitted '.
107
'to log in via the web. Log in as a normal user instead.'))
108
->addCancelButton('/');
109
}
110
111
if ($request->isFormPost() || $is_logged_in) {
112
// If we have an email bound into this URI, verify email so that clicking
113
// the link in the "Welcome" email is good enough, without requiring users
114
// to go through a second round of email verification.
115
116
$editor = id(new PhabricatorUserEditor())
117
->setActor($target_user);
118
119
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
120
// Nuke the token and all other outstanding password reset tokens.
121
// There is no particular security benefit to destroying them all, but
122
// it should reduce HackerOne reports of nebulous harm.
123
$editor->revokePasswordResetLinks($target_user);
124
125
if ($target_email) {
126
$editor->verifyEmail($target_user, $target_email);
127
}
128
unset($unguarded);
129
130
$next_uri = $this->getNextStepURI($target_user);
131
132
// If the user is already logged in, we're just doing a "password set"
133
// flow. Skip directly to the next step.
134
if ($is_logged_in) {
135
return id(new AphrontRedirectResponse())->setURI($next_uri);
136
}
137
138
PhabricatorCookies::setNextURICookie($request, $next_uri, $force = true);
139
140
$force_full_session = false;
141
if ($link_type === PhabricatorAuthSessionEngine::ONETIME_RECOVER) {
142
$force_full_session = $token->getShouldForceFullSession();
143
}
144
145
return $this->loginUser($target_user, $force_full_session);
146
}
147
148
// NOTE: We need to CSRF here so attackers can't generate an email link,
149
// then log a user in to an account they control via sneaky invisible
150
// form submissions.
151
152
switch ($link_type) {
153
case PhabricatorAuthSessionEngine::ONETIME_WELCOME:
154
$title = pht(
155
'Welcome to %s',
156
PlatformSymbols::getPlatformServerName());
157
break;
158
case PhabricatorAuthSessionEngine::ONETIME_RECOVER:
159
$title = pht('Account Recovery');
160
break;
161
case PhabricatorAuthSessionEngine::ONETIME_USERNAME:
162
case PhabricatorAuthSessionEngine::ONETIME_RESET:
163
default:
164
$title = pht(
165
'Log in to %s',
166
PlatformSymbols::getPlatformServerName());
167
break;
168
}
169
170
$body = array();
171
$body[] = pht(
172
'Use the button below to log in as: %s',
173
phutil_tag('strong', array(), $target_user->getUsername()));
174
175
if ($target_email && !$target_email->getIsVerified()) {
176
$body[] = pht(
177
'Logging in will verify %s as an email address you own.',
178
phutil_tag('strong', array(), $target_email->getAddress()));
179
180
}
181
182
$body[] = pht(
183
'After logging in you should set a password for your account, or '.
184
'link your account to an external account that you can use to '.
185
'authenticate in the future.');
186
187
$dialog = $this->newDialog()
188
->setTitle($title)
189
->addSubmitButton(pht('Log In (%s)', $target_user->getUsername()))
190
->addCancelButton('/');
191
192
foreach ($body as $paragraph) {
193
$dialog->appendParagraph($paragraph);
194
}
195
196
return id(new AphrontDialogResponse())->setDialog($dialog);
197
}
198
199
private function getNextStepURI(PhabricatorUser $user) {
200
$request = $this->getRequest();
201
202
// If we have password auth, let the user set or reset their password after
203
// login.
204
$have_passwords = PhabricatorPasswordAuthProvider::getPasswordProvider();
205
if ($have_passwords) {
206
// We're going to let the user reset their password without knowing
207
// the old one. Generate a one-time token for that.
208
$key = Filesystem::readRandomCharacters(16);
209
$password_type =
210
PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;
211
212
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
213
id(new PhabricatorAuthTemporaryToken())
214
->setTokenResource($user->getPHID())
215
->setTokenType($password_type)
216
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
217
->setTokenCode(PhabricatorHash::weakDigest($key))
218
->save();
219
unset($unguarded);
220
221
$panel_uri = '/auth/password/';
222
223
$request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes');
224
225
$params = array(
226
'key' => $key,
227
);
228
229
return (string)new PhutilURI($panel_uri, $params);
230
}
231
232
// Check if the user already has external accounts linked. If they do,
233
// it's not obvious why they aren't using them to log in, but assume they
234
// know what they're doing. We won't send them to the link workflow.
235
$accounts = id(new PhabricatorExternalAccountQuery())
236
->setViewer($user)
237
->withUserPHIDs(array($user->getPHID()))
238
->execute();
239
240
$configs = id(new PhabricatorAuthProviderConfigQuery())
241
->setViewer($user)
242
->withIsEnabled(true)
243
->execute();
244
245
$linkable = array();
246
foreach ($configs as $config) {
247
if (!$config->getShouldAllowLink()) {
248
continue;
249
}
250
251
$provider = $config->getProvider();
252
if (!$provider->isLoginFormAButton()) {
253
continue;
254
}
255
256
$linkable[] = $provider;
257
}
258
259
// If there's at least one linkable provider, and the user doesn't already
260
// have accounts, send the user to the link workflow.
261
if (!$accounts && $linkable) {
262
return '/auth/external/';
263
}
264
265
// If there are no configured providers and the user is an administrator,
266
// send them to Auth to configure a provider. This is probably where they
267
// want to go. You can end up in this state by accidentally losing your
268
// first session during initial setup, or after restoring exported data
269
// from a hosted instance.
270
if (!$configs && $user->getIsAdmin()) {
271
return '/auth/';
272
}
273
274
// If we didn't find anywhere better to send them, give up and just send
275
// them to the home page.
276
return '/';
277
}
278
279
}
280
281