diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php index 8fdd6fdb75..745be3e820 100644 --- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php +++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php @@ -1,317 +1,316 @@ getViewer(); $server = new PhabricatorOAuthServer(); $client_phid = $request->getStr('client_id'); $redirect_uri = $request->getStr('redirect_uri'); $response_type = $request->getStr('response_type'); // state is an opaque value the client sent us for their own purposes // we just need to send it right back to them in the response! $state = $request->getStr('state'); if (!$client_phid) { return $this->buildErrorResponse( 'invalid_request', pht('Malformed Request'), pht( 'Required parameter %s was not present in the request.', phutil_tag('strong', array(), 'client_id'))); } // We require that users must be able to see an OAuth application // in order to authorize it. This allows an application's visibility // policy to be used to restrict authorized users. try { $client = id(new PhabricatorOAuthServerClientQuery()) ->setViewer($viewer) ->withPHIDs(array($client_phid)) ->executeOne(); } catch (PhabricatorPolicyException $ex) { $ex->setContext(self::CONTEXT_AUTHORIZE); throw $ex; } $server->setUser($viewer); $is_authorized = false; $authorization = null; $uri = null; $name = null; // one giant try / catch around all the exciting database stuff so we // can return a 'server_error' response if something goes wrong! try { if (!$client) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Client Application'), pht( 'Request parameter %s does not specify a valid client application.', phutil_tag('strong', array(), 'client_id'))); } if ($client->getIsDisabled()) { return $this->buildErrorResponse( 'invalid_request', pht('Application Disabled'), pht( 'The %s OAuth application has been disabled.', phutil_tag('strong', array(), 'client_id'))); } $name = $client->getName(); $server->setClient($client); if ($redirect_uri) { $client_uri = new PhutilURI($client->getRedirectURI()); $redirect_uri = new PhutilURI($redirect_uri); if (!($server->validateSecondaryRedirectURI($redirect_uri, $client_uri))) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Redirect URI'), pht( 'Request parameter %s specifies an invalid redirect URI. '. 'The redirect URI must be a fully-qualified domain with no '. 'fragments, and must have the same domain and at least '. 'the same query parameters as the redirect URI the client '. 'registered.', phutil_tag('strong', array(), 'redirect_uri'))); } $uri = $redirect_uri; } else { $uri = new PhutilURI($client->getRedirectURI()); } if (empty($response_type)) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Response Type'), pht( 'Required request parameter %s is missing.', phutil_tag('strong', array(), 'response_type'))); } if ($response_type != 'code') { return $this->buildErrorResponse( 'unsupported_response_type', pht('Unsupported Response Type'), pht( 'Request parameter %s specifies an unsupported response type. '. 'Valid response types are: %s.', phutil_tag('strong', array(), 'response_type'), implode(', ', array('code')))); } $requested_scope = $request->getStrList('scope'); $requested_scope = array_fuse($requested_scope); $scope = PhabricatorOAuthServerScope::filterScope($requested_scope); // NOTE: We're always requiring a confirmation dialog to redirect. // Partly this is a general defense against redirect attacks, and // partly this shakes off anchors in the URI (which are not shaken // by 302'ing). $auth_info = $server->userHasAuthorizedClient($scope); list($is_authorized, $authorization) = $auth_info; if ($request->isFormPost()) { if ($authorization) { $authorization->setScope($scope)->save(); } else { $authorization = $server->authorizeClient($scope); } $is_authorized = true; } } catch (Exception $e) { return $this->buildErrorResponse( 'server_error', pht('Server Error'), pht( 'The authorization server encountered an unexpected condition '. 'which prevented it from fulfilling the request.')); } // When we reach this part of the controller, we can be in two states: // // 1. The user has not authorized the application yet. We want to // give them an "Authorize this application?" dialog. // 2. The user has authorized the application. We want to give them // a "Confirm Login" dialog. if ($is_authorized) { // The second case is simpler, so handle it first. The user either // authorized the application previously, or has just authorized the // application. Show them a confirm dialog with a normal link back to // the application. This shakes anchors from the URI. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $auth_code = $server->generateAuthorizationCode($uri); unset($unguarded); $full_uri = $this->addQueryParams( $uri, array( 'code' => $auth_code->getCode(), 'scope' => $authorization->getScopeString(), 'state' => $state, )); if ($client->getIsTrusted()) { // NOTE: See T13099. We currently emit a "Content-Security-Policy" // which includes a narrow "form-action". At the time of writing, // Chrome applies "form-action" to redirects following form submission. // This can lead to a situation where a user enters the OAuth workflow // and is prompted for MFA. When they submit an MFA response, the form // can redirect here, and Chrome will block the "Location" redirect. // To avoid this, render an interstitial. We only actually need to do // this in Chrome (but do it everywhere for consistency) and only need // to do it if the request is a redirect after a form submission (but // we can't tell if it is or not). Javelin::initBehavior( 'redirect', array( 'uri' => (string)$full_uri, )); return $this->newDialog() ->setTitle(pht('Authenticate: %s', $name)) - ->setRedirect(true) ->appendParagraph( pht( 'Authorization for "%s" confirmed, redirecting...', phutil_tag('strong', array(), $name))) ->addCancelButton((string)$full_uri, pht('Continue')); } // TODO: It would be nice to give the user more options here, like // reviewing permissions, canceling the authorization, or aborting // the workflow. $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Authenticate: %s', $name)) ->appendParagraph( pht( 'This application ("%s") is authorized to use your Phabricator '. 'credentials. Continue to complete the authentication workflow.', phutil_tag('strong', array(), $name))) ->addCancelButton((string)$full_uri, pht('Continue to Application')); return id(new AphrontDialogResponse())->setDialog($dialog); } // Here, we're confirming authorization for the application. if ($authorization) { $missing_scope = array_diff_key($scope, $authorization->getScope()); } else { $missing_scope = $scope; } $form = id(new AphrontFormView()) ->addHiddenInput('client_id', $client_phid) ->addHiddenInput('redirect_uri', $redirect_uri) ->addHiddenInput('response_type', $response_type) ->addHiddenInput('state', $state) ->addHiddenInput('scope', $request->getStr('scope')) ->setUser($viewer); $cancel_msg = pht('The user declined to authorize this application.'); $cancel_uri = $this->addQueryParams( $uri, array( 'error' => 'access_denied', 'error_description' => $cancel_msg, )); $dialog = $this->newDialog() ->setShortTitle(pht('Authorize Access')) ->setTitle(pht('Authorize "%s"?', $name)) ->setSubmitURI($request->getRequestURI()->getPath()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendParagraph( pht( 'Do you want to authorize the external application "%s" to '. 'access your Phabricator account data, including your primary '. 'email address?', phutil_tag('strong', array(), $name))) ->appendForm($form) ->addSubmitButton(pht('Authorize Access')) ->addCancelButton((string)$cancel_uri, pht('Do Not Authorize')); if ($missing_scope) { $dialog->appendParagraph( pht( 'This application has requested these additional permissions. '. 'Authorizing it will grant it the permissions it requests:')); foreach ($missing_scope as $scope_key => $ignored) { // TODO: Once we introduce more scopes, explain them here. } } $unknown_scope = array_diff_key($requested_scope, $scope); if ($unknown_scope) { $dialog->appendParagraph( pht( 'This application also requested additional unrecognized '. 'permissions. These permissions may have existed in an older '. 'version of Phabricator, or may be from a future version of '. 'Phabricator. They will not be granted.')); $unknown_form = id(new AphrontFormView()) ->setViewer($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Unknown Scope')) ->setValue(implode(', ', array_keys($unknown_scope))) ->setDisabled(true)); $dialog->appendForm($unknown_form); } return $dialog; } private function buildErrorResponse($code, $title, $message) { $viewer = $this->getRequest()->getUser(); return $this->newDialog() ->setTitle(pht('OAuth: %s', $title)) ->appendParagraph($message) ->appendParagraph( pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code))) ->addCancelButton('/', pht('Alas!')); } private function addQueryParams(PhutilURI $uri, array $params) { $full_uri = clone $uri; foreach ($params as $key => $value) { if (strlen($value)) { $full_uri->setQueryParam($key, $value); } } return $full_uri; } }