@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 272 lines 7.0 kB view raw
1<?php 2 3final 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, self::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, self::class); 58 59 $token_list = $request->getStr(self::HTTPKEY, ''); 60 $token_list = explode(' ', $token_list); 61 62 $token_map = array(); 63 foreach ($token_list as $token_element) { 64 $token_element = trim($token_element, ' '); 65 66 if (!strlen($token_element)) { 67 continue; 68 } 69 70 // NOTE: This error message is intentionally not printing the token to 71 // avoid disclosing it. As a result, it isn't terribly useful, but no 72 // normal user should ever end up here. 73 if (!preg_match('/^[^:]+:/', $token_element)) { 74 throw new Exception( 75 pht( 76 'This request included an improperly formatted MFA challenge '. 77 'token and can not be processed.')); 78 } 79 80 list($phid, $token) = explode(':', $token_element, 2); 81 82 if (isset($token_map[$phid])) { 83 throw new Exception( 84 pht( 85 'This request improperly specifies an MFA challenge token ("%s") '. 86 'multiple times and can not be processed.', 87 $phid)); 88 } 89 90 $token_map[$phid] = new PhutilOpaqueEnvelope($token); 91 } 92 93 $challenges = mpull($challenges, null, 'getPHID'); 94 95 $now = PhabricatorTime::getNow(); 96 foreach ($challenges as $challenge_phid => $challenge) { 97 // If the response window has expired, don't attach the token. 98 if ($challenge->getResponseTTL() < $now) { 99 continue; 100 } 101 102 $token = idx($token_map, $challenge_phid); 103 if (!$token) { 104 continue; 105 } 106 107 $challenge->setResponseToken($token); 108 } 109 } 110 111 112 protected function getConfiguration() { 113 return array( 114 self::CONFIG_SERIALIZATION => array( 115 'properties' => self::SERIALIZATION_JSON, 116 ), 117 self::CONFIG_AUX_PHID => true, 118 self::CONFIG_COLUMN_SCHEMA => array( 119 'challengeKey' => 'text255', 120 'challengeTTL' => 'epoch', 121 'workflowKey' => 'text255', 122 'responseDigest' => 'text255?', 123 'responseTTL' => 'epoch?', 124 'isCompleted' => 'bool', 125 ), 126 self::CONFIG_KEY_SCHEMA => array( 127 'key_issued' => array( 128 'columns' => array('userPHID', 'challengeTTL'), 129 ), 130 'key_collection' => array( 131 'columns' => array('challengeTTL'), 132 ), 133 ), 134 ) + parent::getConfiguration(); 135 } 136 137 public function getPHIDType() { 138 return PhabricatorAuthChallengePHIDType::TYPECONST; 139 } 140 141 public function getIsReusedChallenge() { 142 if ($this->getIsCompleted()) { 143 return true; 144 } 145 146 if (!$this->getIsAnsweredChallenge()) { 147 return false; 148 } 149 150 // If the challenge has been answered but the client has provided a token 151 // proving that they answered it, this is still a valid response. 152 if ($this->getResponseToken()) { 153 return false; 154 } 155 156 return true; 157 } 158 159 public function getIsAnsweredChallenge() { 160 return (bool)$this->getResponseDigest(); 161 } 162 163 public function markChallengeAsAnswered($ttl) { 164 $token = Filesystem::readRandomCharacters(32); 165 $token = new PhutilOpaqueEnvelope($token); 166 167 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 168 169 $this 170 ->setResponseToken($token) 171 ->setResponseTTL($ttl) 172 ->save(); 173 174 unset($unguarded); 175 176 return $this; 177 } 178 179 public function markChallengeAsCompleted() { 180 return $this 181 ->setIsCompleted(true) 182 ->save(); 183 } 184 185 public function setResponseToken(PhutilOpaqueEnvelope $token) { 186 if (!$this->getUserPHID()) { 187 throw new PhutilInvalidStateException('setUserPHID'); 188 } 189 190 if ($this->responseToken) { 191 throw new Exception( 192 pht( 193 'This challenge already has a response token; you can not '. 194 'set a new response token.')); 195 } 196 197 if (preg_match('/ /', $token->openEnvelope())) { 198 throw new Exception( 199 pht( 200 'The response token for this challenge is invalid: response '. 201 'tokens may not include spaces.')); 202 } 203 204 $digest = PhabricatorHash::digestWithNamedKey( 205 $token->openEnvelope(), 206 self::TOKEN_DIGEST_KEY); 207 208 if ($this->responseDigest !== null) { 209 if (!phutil_hashes_are_identical($digest, $this->responseDigest)) { 210 throw new Exception( 211 pht( 212 'Invalid response token for this challenge: token digest does '. 213 'not match stored digest.')); 214 } 215 } else { 216 $this->responseDigest = $digest; 217 } 218 219 $this->responseToken = $token; 220 221 return $this; 222 } 223 224 public function getResponseToken() { 225 return $this->responseToken; 226 } 227 228 public function setResponseDigest($value) { 229 throw new Exception( 230 pht( 231 'You can not set the response digest for a challenge directly. '. 232 'Instead, set a response token. A response digest will be computed '. 233 'automatically.')); 234 } 235 236 public function setProperty($key, $value) { 237 $this->properties[$key] = $value; 238 return $this; 239 } 240 241 public function getProperty($key, $default = null) { 242 return $this->properties[$key]; 243 } 244 245 public function setIsNewChallenge($is_new_challenge) { 246 $this->isNewChallenge = $is_new_challenge; 247 return $this; 248 } 249 250 public function getIsNewChallenge() { 251 return $this->isNewChallenge; 252 } 253 254 255/* -( PhabricatorPolicyInterface )----------------------------------------- */ 256 257 258 public function getCapabilities() { 259 return array( 260 PhabricatorPolicyCapability::CAN_VIEW, 261 ); 262 } 263 264 public function getPolicy($capability) { 265 return PhabricatorPolicies::POLICY_NOONE; 266 } 267 268 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 269 return ($viewer->getPHID() === $this->getUserPHID()); 270 } 271 272}