Path: blob/master/src/applications/auth/provider/PhabricatorOAuth2AuthProvider.php
12256 views
<?php12abstract class PhabricatorOAuth2AuthProvider3extends PhabricatorOAuthAuthProvider {45const PROPERTY_APP_ID = 'oauth:app:id';6const PROPERTY_APP_SECRET = 'oauth:app:secret';78protected function getIDKey() {9return self::PROPERTY_APP_ID;10}1112protected function getSecretKey() {13return self::PROPERTY_APP_SECRET;14}151617protected function configureAdapter(PhutilOAuthAuthAdapter $adapter) {18$config = $this->getProviderConfig();19$adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID));20$adapter->setClientSecret(21new PhutilOpaqueEnvelope(22$config->getProperty(self::PROPERTY_APP_SECRET)));23$adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI()));24return $adapter;25}2627protected function renderLoginForm(AphrontRequest $request, $mode) {28$adapter = $this->getAdapter();29$adapter->setState($this->getAuthCSRFCode($request));3031$scope = $request->getStr('scope');32if ($scope) {33$adapter->setScope($scope);34}3536$attributes = array(37'method' => 'GET',38'uri' => $adapter->getAuthenticateURI(),39);4041return $this->renderStandardLoginButton($request, $mode, $attributes);42}4344public function processLoginRequest(45PhabricatorAuthLoginController $controller) {4647$request = $controller->getRequest();48$adapter = $this->getAdapter();49$account = null;50$response = null;5152$error = $request->getStr('error');53if ($error) {54$response = $controller->buildProviderErrorResponse(55$this,56pht(57'The OAuth provider returned an error: %s',58$error));5960return array($account, $response);61}6263$this->verifyAuthCSRFCode($request, $request->getStr('state'));6465$code = $request->getStr('code');66if (!strlen($code)) {67$response = $controller->buildProviderErrorResponse(68$this,69pht(70'The OAuth provider did not return a "code" parameter in its '.71'response.'));7273return array($account, $response);74}7576$adapter->setCode($code);7778// NOTE: As a side effect, this will cause the OAuth adapter to request79// an access token.8081try {82$identifiers = $adapter->getAccountIdentifiers();83} catch (Exception $ex) {84// TODO: Handle this in a more user-friendly way.85throw $ex;86}8788if (!$identifiers) {89$response = $controller->buildProviderErrorResponse(90$this,91pht(92'The OAuth provider failed to retrieve an account ID.'));9394return array($account, $response);95}9697$account = $this->newExternalAccountForIdentifiers($identifiers);9899return array($account, $response);100}101102public function processEditForm(103AphrontRequest $request,104array $values) {105106return $this->processOAuthEditForm(107$request,108$values,109pht('Application ID is required.'),110pht('Application secret is required.'));111}112113public function extendEditForm(114AphrontRequest $request,115AphrontFormView $form,116array $values,117array $issues) {118119return $this->extendOAuthEditForm(120$request,121$form,122$values,123$issues,124pht('OAuth App ID'),125pht('OAuth App Secret'));126}127128public function renderConfigPropertyTransactionTitle(129PhabricatorAuthProviderConfigTransaction $xaction) {130131$author_phid = $xaction->getAuthorPHID();132$old = $xaction->getOldValue();133$new = $xaction->getNewValue();134$key = $xaction->getMetadataValue(135PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);136137switch ($key) {138case self::PROPERTY_APP_ID:139if (strlen($old)) {140return pht(141'%s updated the OAuth application ID for this provider from '.142'"%s" to "%s".',143$xaction->renderHandleLink($author_phid),144$old,145$new);146} else {147return pht(148'%s set the OAuth application ID for this provider to '.149'"%s".',150$xaction->renderHandleLink($author_phid),151$new);152}153case self::PROPERTY_APP_SECRET:154if (strlen($old)) {155return pht(156'%s updated the OAuth application secret for this provider.',157$xaction->renderHandleLink($author_phid));158} else {159return pht(160'%s set the OAuth application secret for this provider.',161$xaction->renderHandleLink($author_phid));162}163case self::PROPERTY_NOTE:164if (strlen($old)) {165return pht(166'%s updated the OAuth application notes for this provider.',167$xaction->renderHandleLink($author_phid));168} else {169return pht(170'%s set the OAuth application notes for this provider.',171$xaction->renderHandleLink($author_phid));172}173174}175176return parent::renderConfigPropertyTransactionTitle($xaction);177}178179protected function synchronizeOAuthAccount(180PhabricatorExternalAccount $account) {181$adapter = $this->getAdapter();182183$oauth_token = $adapter->getAccessToken();184$account->setProperty('oauth.token.access', $oauth_token);185186if ($adapter->supportsTokenRefresh()) {187$refresh_token = $adapter->getRefreshToken();188$account->setProperty('oauth.token.refresh', $refresh_token);189} else {190$account->setProperty('oauth.token.refresh', null);191}192193$expires = $adapter->getAccessTokenExpires();194$account->setProperty('oauth.token.access.expires', $expires);195}196197public function getOAuthAccessToken(198PhabricatorExternalAccount $account,199$force_refresh = false) {200201if ($account->getProviderConfigPHID() !== $this->getProviderConfigPHID()) {202throw new Exception(pht('Account does not match provider!'));203}204205if (!$force_refresh) {206$access_expires = $account->getProperty('oauth.token.access.expires');207$access_token = $account->getProperty('oauth.token.access');208209// Don't return a token with fewer than this many seconds remaining until210// it expires.211$shortest_token = 60;212if ($access_token) {213if ($access_expires === null ||214$access_expires > (time() + $shortest_token)) {215return $access_token;216}217}218}219220$refresh_token = $account->getProperty('oauth.token.refresh');221if ($refresh_token) {222$adapter = $this->getAdapter();223if ($adapter->supportsTokenRefresh()) {224$adapter->refreshAccessToken($refresh_token);225226$this->synchronizeOAuthAccount($account);227$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();228$account->save();229unset($unguarded);230231return $account->getProperty('oauth.token.access');232}233}234235return null;236}237238public function willRenderLinkedAccount(239PhabricatorUser $viewer,240PHUIObjectItemView $item,241PhabricatorExternalAccount $account) {242243// Get a valid token, possibly refreshing it. If we're unable to refresh244// it, render a message to that effect. The user may be able to repair the245// link by manually reconnecting.246247$is_invalid = false;248try {249$oauth_token = $this->getOAuthAccessToken($account);250} catch (Exception $ex) {251$oauth_token = null;252$is_invalid = true;253}254255$item->addAttribute(pht('OAuth2 Account'));256257if ($oauth_token) {258$oauth_expires = $account->getProperty('oauth.token.access.expires');259if ($oauth_expires) {260$item->addAttribute(261pht(262'Active OAuth Token (Expires: %s)',263phabricator_datetime($oauth_expires, $viewer)));264} else {265$item->addAttribute(266pht('Active OAuth Token'));267}268} else if ($is_invalid) {269$item->addAttribute(pht('Invalid OAuth Access Token'));270} else {271$item->addAttribute(pht('No OAuth Access Token'));272}273274parent::willRenderLinkedAccount($viewer, $item, $account);275}276277public function supportsAutoLogin() {278return true;279}280281public function getAutoLoginURI(AphrontRequest $request) {282$csrf_code = $this->getAuthCSRFCode($request);283284$adapter = $this->getAdapter();285$adapter->setState($csrf_code);286287return $adapter->getAuthenticateURI();288}289290}291292293