Path: blob/master/src/applications/auth/storage/PhabricatorAuthChallenge.php
12256 views
<?php12final class PhabricatorAuthChallenge3extends PhabricatorAuthDAO4implements PhabricatorPolicyInterface {56protected $userPHID;7protected $factorPHID;8protected $sessionPHID;9protected $workflowKey;10protected $challengeKey;11protected $challengeTTL;12protected $responseDigest;13protected $responseTTL;14protected $isCompleted;15protected $properties = array();1617private $responseToken;18private $isNewChallenge;1920const HTTPKEY = '__hisec.challenges__';21const TOKEN_DIGEST_KEY = 'auth.challenge.token';2223public static function initializeNewChallenge() {24return id(new self())25->setIsCompleted(0);26}2728public static function newHTTPParametersFromChallenges(array $challenges) {29assert_instances_of($challenges, __CLASS__);3031$token_list = array();32foreach ($challenges as $challenge) {33$token = $challenge->getResponseToken();34if ($token) {35$token_list[] = sprintf(36'%s:%s',37$challenge->getPHID(),38$token->openEnvelope());39}40}4142if (!$token_list) {43return array();44}4546$token_list = implode(' ', $token_list);4748return array(49self::HTTPKEY => $token_list,50);51}5253public static function newChallengeResponsesFromRequest(54array $challenges,55AphrontRequest $request) {56assert_instances_of($challenges, __CLASS__);5758$token_list = $request->getStr(self::HTTPKEY);59if ($token_list === null) {60return;61}62$token_list = explode(' ', $token_list);6364$token_map = array();65foreach ($token_list as $token_element) {66$token_element = trim($token_element, ' ');6768if (!strlen($token_element)) {69continue;70}7172// NOTE: This error message is intentionally not printing the token to73// avoid disclosing it. As a result, it isn't terribly useful, but no74// normal user should ever end up here.75if (!preg_match('/^[^:]+:/', $token_element)) {76throw new Exception(77pht(78'This request included an improperly formatted MFA challenge '.79'token and can not be processed.'));80}8182list($phid, $token) = explode(':', $token_element, 2);8384if (isset($token_map[$phid])) {85throw new Exception(86pht(87'This request improperly specifies an MFA challenge token ("%s") '.88'multiple times and can not be processed.',89$phid));90}9192$token_map[$phid] = new PhutilOpaqueEnvelope($token);93}9495$challenges = mpull($challenges, null, 'getPHID');9697$now = PhabricatorTime::getNow();98foreach ($challenges as $challenge_phid => $challenge) {99// If the response window has expired, don't attach the token.100if ($challenge->getResponseTTL() < $now) {101continue;102}103104$token = idx($token_map, $challenge_phid);105if (!$token) {106continue;107}108109$challenge->setResponseToken($token);110}111}112113114protected function getConfiguration() {115return array(116self::CONFIG_SERIALIZATION => array(117'properties' => self::SERIALIZATION_JSON,118),119self::CONFIG_AUX_PHID => true,120self::CONFIG_COLUMN_SCHEMA => array(121'challengeKey' => 'text255',122'challengeTTL' => 'epoch',123'workflowKey' => 'text255',124'responseDigest' => 'text255?',125'responseTTL' => 'epoch?',126'isCompleted' => 'bool',127),128self::CONFIG_KEY_SCHEMA => array(129'key_issued' => array(130'columns' => array('userPHID', 'challengeTTL'),131),132'key_collection' => array(133'columns' => array('challengeTTL'),134),135),136) + parent::getConfiguration();137}138139public function getPHIDType() {140return PhabricatorAuthChallengePHIDType::TYPECONST;141}142143public function getIsReusedChallenge() {144if ($this->getIsCompleted()) {145return true;146}147148if (!$this->getIsAnsweredChallenge()) {149return false;150}151152// If the challenge has been answered but the client has provided a token153// proving that they answered it, this is still a valid response.154if ($this->getResponseToken()) {155return false;156}157158return true;159}160161public function getIsAnsweredChallenge() {162return (bool)$this->getResponseDigest();163}164165public function markChallengeAsAnswered($ttl) {166$token = Filesystem::readRandomCharacters(32);167$token = new PhutilOpaqueEnvelope($token);168169$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();170171$this172->setResponseToken($token)173->setResponseTTL($ttl)174->save();175176unset($unguarded);177178return $this;179}180181public function markChallengeAsCompleted() {182return $this183->setIsCompleted(true)184->save();185}186187public function setResponseToken(PhutilOpaqueEnvelope $token) {188if (!$this->getUserPHID()) {189throw new PhutilInvalidStateException('setUserPHID');190}191192if ($this->responseToken) {193throw new Exception(194pht(195'This challenge already has a response token; you can not '.196'set a new response token.'));197}198199if (preg_match('/ /', $token->openEnvelope())) {200throw new Exception(201pht(202'The response token for this challenge is invalid: response '.203'tokens may not include spaces.'));204}205206$digest = PhabricatorHash::digestWithNamedKey(207$token->openEnvelope(),208self::TOKEN_DIGEST_KEY);209210if ($this->responseDigest !== null) {211if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {212throw new Exception(213pht(214'Invalid response token for this challenge: token digest does '.215'not match stored digest.'));216}217} else {218$this->responseDigest = $digest;219}220221$this->responseToken = $token;222223return $this;224}225226public function getResponseToken() {227return $this->responseToken;228}229230public function setResponseDigest($value) {231throw new Exception(232pht(233'You can not set the response digest for a challenge directly. '.234'Instead, set a response token. A response digest will be computed '.235'automatically.'));236}237238public function setProperty($key, $value) {239$this->properties[$key] = $value;240return $this;241}242243public function getProperty($key, $default = null) {244return $this->properties[$key];245}246247public function setIsNewChallenge($is_new_challenge) {248$this->isNewChallenge = $is_new_challenge;249return $this;250}251252public function getIsNewChallenge() {253return $this->isNewChallenge;254}255256257/* -( PhabricatorPolicyInterface )----------------------------------------- */258259260public function getCapabilities() {261return array(262PhabricatorPolicyCapability::CAN_VIEW,263);264}265266public function getPolicy($capability) {267return PhabricatorPolicies::POLICY_NOONE;268}269270public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {271return ($viewer->getPHID() === $this->getUserPHID());272}273274}275276277