Path: blob/master/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
12242 views
<?php12final class PhabricatorOAuthServerAuthController3extends PhabricatorOAuthServerController {45protected function buildApplicationCrumbs() {6// We're specifically not putting an "OAuth Server" application crumb7// on the auth pages because it doesn't make sense to send users there.8return new PHUICrumbsView();9}1011public function handleRequest(AphrontRequest $request) {12$viewer = $this->getViewer();1314$server = new PhabricatorOAuthServer();15$client_phid = $request->getStr('client_id');16$redirect_uri = $request->getStr('redirect_uri');17$response_type = $request->getStr('response_type');1819// state is an opaque value the client sent us for their own purposes20// we just need to send it right back to them in the response!21$state = $request->getStr('state');2223if (!$client_phid) {24return $this->buildErrorResponse(25'invalid_request',26pht('Malformed Request'),27pht(28'Required parameter %s was not present in the request.',29phutil_tag('strong', array(), 'client_id')));30}3132// We require that users must be able to see an OAuth application33// in order to authorize it. This allows an application's visibility34// policy to be used to restrict authorized users.35try {36$client = id(new PhabricatorOAuthServerClientQuery())37->setViewer($viewer)38->withPHIDs(array($client_phid))39->executeOne();40} catch (PhabricatorPolicyException $ex) {41$ex->setContext(self::CONTEXT_AUTHORIZE);42throw $ex;43}4445$server->setUser($viewer);46$is_authorized = false;47$authorization = null;48$uri = null;49$name = null;5051// one giant try / catch around all the exciting database stuff so we52// can return a 'server_error' response if something goes wrong!53try {54if (!$client) {55return $this->buildErrorResponse(56'invalid_request',57pht('Invalid Client Application'),58pht(59'Request parameter %s does not specify a valid client application.',60phutil_tag('strong', array(), 'client_id')));61}6263if ($client->getIsDisabled()) {64return $this->buildErrorResponse(65'invalid_request',66pht('Application Disabled'),67pht(68'The %s OAuth application has been disabled.',69phutil_tag('strong', array(), 'client_id')));70}7172$name = $client->getName();73$server->setClient($client);74if ($redirect_uri) {75$client_uri = new PhutilURI($client->getRedirectURI());76$redirect_uri = new PhutilURI($redirect_uri);77if (!($server->validateSecondaryRedirectURI($redirect_uri,78$client_uri))) {79return $this->buildErrorResponse(80'invalid_request',81pht('Invalid Redirect URI'),82pht(83'Request parameter %s specifies an invalid redirect URI. '.84'The redirect URI must be a fully-qualified domain with no '.85'fragments, and must have the same domain and at least '.86'the same query parameters as the redirect URI the client '.87'registered.',88phutil_tag('strong', array(), 'redirect_uri')));89}90$uri = $redirect_uri;91} else {92$uri = new PhutilURI($client->getRedirectURI());93}9495if (empty($response_type)) {96return $this->buildErrorResponse(97'invalid_request',98pht('Invalid Response Type'),99pht(100'Required request parameter %s is missing.',101phutil_tag('strong', array(), 'response_type')));102}103104if ($response_type != 'code') {105return $this->buildErrorResponse(106'unsupported_response_type',107pht('Unsupported Response Type'),108pht(109'Request parameter %s specifies an unsupported response type. '.110'Valid response types are: %s.',111phutil_tag('strong', array(), 'response_type'),112implode(', ', array('code'))));113}114115116$requested_scope = $request->getStrList('scope');117$requested_scope = array_fuse($requested_scope);118119$scope = PhabricatorOAuthServerScope::filterScope($requested_scope);120121// NOTE: We're always requiring a confirmation dialog to redirect.122// Partly this is a general defense against redirect attacks, and123// partly this shakes off anchors in the URI (which are not shaken124// by 302'ing).125126$auth_info = $server->userHasAuthorizedClient($scope);127list($is_authorized, $authorization) = $auth_info;128129if ($request->isFormPost()) {130if ($authorization) {131$authorization->setScope($scope)->save();132} else {133$authorization = $server->authorizeClient($scope);134}135136$is_authorized = true;137}138} catch (Exception $e) {139return $this->buildErrorResponse(140'server_error',141pht('Server Error'),142pht(143'The authorization server encountered an unexpected condition '.144'which prevented it from fulfilling the request.'));145}146147// When we reach this part of the controller, we can be in two states:148//149// 1. The user has not authorized the application yet. We want to150// give them an "Authorize this application?" dialog.151// 2. The user has authorized the application. We want to give them152// a "Confirm Login" dialog.153154if ($is_authorized) {155156// The second case is simpler, so handle it first. The user either157// authorized the application previously, or has just authorized the158// application. Show them a confirm dialog with a normal link back to159// the application. This shakes anchors from the URI.160161$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();162$auth_code = $server->generateAuthorizationCode($uri);163unset($unguarded);164165$full_uri = $this->addQueryParams(166$uri,167array(168'code' => $auth_code->getCode(),169'scope' => $authorization->getScopeString(),170'state' => $state,171));172173if ($client->getIsTrusted()) {174// NOTE: See T13099. We currently emit a "Content-Security-Policy"175// which includes a narrow "form-action". At the time of writing,176// Chrome applies "form-action" to redirects following form submission.177178// This can lead to a situation where a user enters the OAuth workflow179// and is prompted for MFA. When they submit an MFA response, the form180// can redirect here, and Chrome will block the "Location" redirect.181182// To avoid this, render an interstitial. We only actually need to do183// this in Chrome (but do it everywhere for consistency) and only need184// to do it if the request is a redirect after a form submission (but185// we can't tell if it is or not).186187Javelin::initBehavior(188'redirect',189array(190'uri' => (string)$full_uri,191));192193return $this->newDialog()194->setTitle(pht('Authenticate: %s', $name))195->appendParagraph(196pht(197'Authorization for "%s" confirmed, redirecting...',198phutil_tag('strong', array(), $name)))199->addCancelButton((string)$full_uri, pht('Continue'));200}201202// TODO: It would be nice to give the user more options here, like203// reviewing permissions, canceling the authorization, or aborting204// the workflow.205206$dialog = id(new AphrontDialogView())207->setUser($viewer)208->setTitle(pht('Authenticate: %s', $name))209->appendParagraph(210pht(211'This application ("%s") is authorized to use your %s '.212'credentials. Continue to complete the authentication workflow.',213phutil_tag('strong', array(), $name),214PlatformSymbols::getPlatformServerName()))215->addCancelButton((string)$full_uri, pht('Continue to Application'));216217return id(new AphrontDialogResponse())->setDialog($dialog);218}219220// Here, we're confirming authorization for the application.221if ($authorization) {222$missing_scope = array_diff_key($scope, $authorization->getScope());223} else {224$missing_scope = $scope;225}226227$form = id(new AphrontFormView())228->addHiddenInput('client_id', $client_phid)229->addHiddenInput('redirect_uri', $redirect_uri)230->addHiddenInput('response_type', $response_type)231->addHiddenInput('state', $state)232->addHiddenInput('scope', $request->getStr('scope'))233->setUser($viewer);234235$cancel_msg = pht('The user declined to authorize this application.');236$cancel_uri = $this->addQueryParams(237$uri,238array(239'error' => 'access_denied',240'error_description' => $cancel_msg,241));242243$dialog = $this->newDialog()244->setShortTitle(pht('Authorize Access'))245->setTitle(pht('Authorize "%s"?', $name))246->setSubmitURI($request->getRequestURI()->getPath())247->setWidth(AphrontDialogView::WIDTH_FORM)248->appendParagraph(249pht(250'Do you want to authorize the external application "%s" to '.251'access your %s account data, including your primary '.252'email address?',253phutil_tag('strong', array(), $name),254PlatformSymbols::getPlatformServerName()))255->appendForm($form)256->addSubmitButton(pht('Authorize Access'))257->addCancelButton((string)$cancel_uri, pht('Do Not Authorize'));258259if ($missing_scope) {260$dialog->appendParagraph(261pht(262'This application has requested these additional permissions. '.263'Authorizing it will grant it the permissions it requests:'));264foreach ($missing_scope as $scope_key => $ignored) {265// TODO: Once we introduce more scopes, explain them here.266}267}268269$unknown_scope = array_diff_key($requested_scope, $scope);270if ($unknown_scope) {271$dialog->appendParagraph(272pht(273'This application also requested additional unrecognized '.274'permissions. These permissions may have existed in an older '.275'version of the software, or may be from a future version of '.276'the software. They will not be granted.'));277278$unknown_form = id(new AphrontFormView())279->setViewer($viewer)280->appendChild(281id(new AphrontFormTextControl())282->setLabel(pht('Unknown Scope'))283->setValue(implode(', ', array_keys($unknown_scope)))284->setDisabled(true));285286$dialog->appendForm($unknown_form);287}288289return $dialog;290}291292293private function buildErrorResponse($code, $title, $message) {294$viewer = $this->getRequest()->getUser();295296return $this->newDialog()297->setTitle(pht('OAuth: %s', $title))298->appendParagraph($message)299->appendParagraph(300pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code)))301->addCancelButton('/', pht('Alas!'));302}303304305private function addQueryParams(PhutilURI $uri, array $params) {306$full_uri = clone $uri;307308foreach ($params as $key => $value) {309if (strlen($value)) {310$full_uri->replaceQueryParam($key, $value);311}312}313314return $full_uri;315}316317}318319320