Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/storage/PhabricatorAuthChallenge.php
12256 views
1
<?php
2
3
final class PhabricatorAuthChallenge
4
extends PhabricatorAuthDAO
5
implements PhabricatorPolicyInterface {
6
7
protected $userPHID;
8
protected $factorPHID;
9
protected $sessionPHID;
10
protected $workflowKey;
11
protected $challengeKey;
12
protected $challengeTTL;
13
protected $responseDigest;
14
protected $responseTTL;
15
protected $isCompleted;
16
protected $properties = array();
17
18
private $responseToken;
19
private $isNewChallenge;
20
21
const HTTPKEY = '__hisec.challenges__';
22
const TOKEN_DIGEST_KEY = 'auth.challenge.token';
23
24
public static function initializeNewChallenge() {
25
return id(new self())
26
->setIsCompleted(0);
27
}
28
29
public static function newHTTPParametersFromChallenges(array $challenges) {
30
assert_instances_of($challenges, __CLASS__);
31
32
$token_list = array();
33
foreach ($challenges as $challenge) {
34
$token = $challenge->getResponseToken();
35
if ($token) {
36
$token_list[] = sprintf(
37
'%s:%s',
38
$challenge->getPHID(),
39
$token->openEnvelope());
40
}
41
}
42
43
if (!$token_list) {
44
return array();
45
}
46
47
$token_list = implode(' ', $token_list);
48
49
return array(
50
self::HTTPKEY => $token_list,
51
);
52
}
53
54
public static function newChallengeResponsesFromRequest(
55
array $challenges,
56
AphrontRequest $request) {
57
assert_instances_of($challenges, __CLASS__);
58
59
$token_list = $request->getStr(self::HTTPKEY);
60
if ($token_list === null) {
61
return;
62
}
63
$token_list = explode(' ', $token_list);
64
65
$token_map = array();
66
foreach ($token_list as $token_element) {
67
$token_element = trim($token_element, ' ');
68
69
if (!strlen($token_element)) {
70
continue;
71
}
72
73
// NOTE: This error message is intentionally not printing the token to
74
// avoid disclosing it. As a result, it isn't terribly useful, but no
75
// normal user should ever end up here.
76
if (!preg_match('/^[^:]+:/', $token_element)) {
77
throw new Exception(
78
pht(
79
'This request included an improperly formatted MFA challenge '.
80
'token and can not be processed.'));
81
}
82
83
list($phid, $token) = explode(':', $token_element, 2);
84
85
if (isset($token_map[$phid])) {
86
throw new Exception(
87
pht(
88
'This request improperly specifies an MFA challenge token ("%s") '.
89
'multiple times and can not be processed.',
90
$phid));
91
}
92
93
$token_map[$phid] = new PhutilOpaqueEnvelope($token);
94
}
95
96
$challenges = mpull($challenges, null, 'getPHID');
97
98
$now = PhabricatorTime::getNow();
99
foreach ($challenges as $challenge_phid => $challenge) {
100
// If the response window has expired, don't attach the token.
101
if ($challenge->getResponseTTL() < $now) {
102
continue;
103
}
104
105
$token = idx($token_map, $challenge_phid);
106
if (!$token) {
107
continue;
108
}
109
110
$challenge->setResponseToken($token);
111
}
112
}
113
114
115
protected function getConfiguration() {
116
return array(
117
self::CONFIG_SERIALIZATION => array(
118
'properties' => self::SERIALIZATION_JSON,
119
),
120
self::CONFIG_AUX_PHID => true,
121
self::CONFIG_COLUMN_SCHEMA => array(
122
'challengeKey' => 'text255',
123
'challengeTTL' => 'epoch',
124
'workflowKey' => 'text255',
125
'responseDigest' => 'text255?',
126
'responseTTL' => 'epoch?',
127
'isCompleted' => 'bool',
128
),
129
self::CONFIG_KEY_SCHEMA => array(
130
'key_issued' => array(
131
'columns' => array('userPHID', 'challengeTTL'),
132
),
133
'key_collection' => array(
134
'columns' => array('challengeTTL'),
135
),
136
),
137
) + parent::getConfiguration();
138
}
139
140
public function getPHIDType() {
141
return PhabricatorAuthChallengePHIDType::TYPECONST;
142
}
143
144
public function getIsReusedChallenge() {
145
if ($this->getIsCompleted()) {
146
return true;
147
}
148
149
if (!$this->getIsAnsweredChallenge()) {
150
return false;
151
}
152
153
// If the challenge has been answered but the client has provided a token
154
// proving that they answered it, this is still a valid response.
155
if ($this->getResponseToken()) {
156
return false;
157
}
158
159
return true;
160
}
161
162
public function getIsAnsweredChallenge() {
163
return (bool)$this->getResponseDigest();
164
}
165
166
public function markChallengeAsAnswered($ttl) {
167
$token = Filesystem::readRandomCharacters(32);
168
$token = new PhutilOpaqueEnvelope($token);
169
170
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
171
172
$this
173
->setResponseToken($token)
174
->setResponseTTL($ttl)
175
->save();
176
177
unset($unguarded);
178
179
return $this;
180
}
181
182
public function markChallengeAsCompleted() {
183
return $this
184
->setIsCompleted(true)
185
->save();
186
}
187
188
public function setResponseToken(PhutilOpaqueEnvelope $token) {
189
if (!$this->getUserPHID()) {
190
throw new PhutilInvalidStateException('setUserPHID');
191
}
192
193
if ($this->responseToken) {
194
throw new Exception(
195
pht(
196
'This challenge already has a response token; you can not '.
197
'set a new response token.'));
198
}
199
200
if (preg_match('/ /', $token->openEnvelope())) {
201
throw new Exception(
202
pht(
203
'The response token for this challenge is invalid: response '.
204
'tokens may not include spaces.'));
205
}
206
207
$digest = PhabricatorHash::digestWithNamedKey(
208
$token->openEnvelope(),
209
self::TOKEN_DIGEST_KEY);
210
211
if ($this->responseDigest !== null) {
212
if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {
213
throw new Exception(
214
pht(
215
'Invalid response token for this challenge: token digest does '.
216
'not match stored digest.'));
217
}
218
} else {
219
$this->responseDigest = $digest;
220
}
221
222
$this->responseToken = $token;
223
224
return $this;
225
}
226
227
public function getResponseToken() {
228
return $this->responseToken;
229
}
230
231
public function setResponseDigest($value) {
232
throw new Exception(
233
pht(
234
'You can not set the response digest for a challenge directly. '.
235
'Instead, set a response token. A response digest will be computed '.
236
'automatically.'));
237
}
238
239
public function setProperty($key, $value) {
240
$this->properties[$key] = $value;
241
return $this;
242
}
243
244
public function getProperty($key, $default = null) {
245
return $this->properties[$key];
246
}
247
248
public function setIsNewChallenge($is_new_challenge) {
249
$this->isNewChallenge = $is_new_challenge;
250
return $this;
251
}
252
253
public function getIsNewChallenge() {
254
return $this->isNewChallenge;
255
}
256
257
258
/* -( PhabricatorPolicyInterface )----------------------------------------- */
259
260
261
public function getCapabilities() {
262
return array(
263
PhabricatorPolicyCapability::CAN_VIEW,
264
);
265
}
266
267
public function getPolicy($capability) {
268
return PhabricatorPolicies::POLICY_NOONE;
269
}
270
271
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
272
return ($viewer->getPHID() === $this->getUserPHID());
273
}
274
275
}
276
277