mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-14 19:02:41 +01:00
03639a7c1e
Summary: Fixes T7153. Test Plan: used `bin/auth trust-oauth-client` and `bin/auth untrust-oauth-client` to set the bit and verify error states. registered via oauth with `bin/auth trust-oauth-client` set and I did not have the confirmation screen registered via oauth with `bin/auth untrust-oauth-client` set and I did have the confirmation screen Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin, epriestley Maniphest Tasks: T7153 Differential Revision: https://secure.phabricator.com/D11724
284 lines
9.8 KiB
PHP
284 lines
9.8 KiB
PHP
<?php
|
|
|
|
final class PhabricatorOAuthServerAuthController
|
|
extends PhabricatorAuthController {
|
|
|
|
public function shouldRequireLogin() {
|
|
return true;
|
|
}
|
|
|
|
public function processRequest() {
|
|
$request = $this->getRequest();
|
|
$viewer = $request->getUser();
|
|
|
|
$server = new PhabricatorOAuthServer();
|
|
$client_phid = $request->getStr('client_id');
|
|
$scope = $request->getStr('scope');
|
|
$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')));
|
|
}
|
|
|
|
$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 {
|
|
try {
|
|
$client = id(new PhabricatorOAuthServerClientQuery())
|
|
->setViewer($viewer)
|
|
->withPHIDs(array($client_phid))
|
|
->executeOne();
|
|
} catch (PhabricatorPolicyException $ex) {
|
|
// 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.
|
|
|
|
// None of the OAuth error responses are a perfect fit for this, but
|
|
// 'invalid_client' seems closest.
|
|
return $this->buildErrorResponse(
|
|
'invalid_client',
|
|
pht('Not Authorized'),
|
|
pht('You are not authorized to authenticate.'));
|
|
}
|
|
|
|
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')));
|
|
}
|
|
|
|
$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'))));
|
|
}
|
|
|
|
if ($scope) {
|
|
if (!PhabricatorOAuthServerScope::validateScopesList($scope)) {
|
|
return $this->buildErrorResponse(
|
|
'invalid_scope',
|
|
pht('Invalid Scope'),
|
|
pht(
|
|
'Request parameter %s specifies an unsupported scope.',
|
|
phutil_tag('strong', array(), 'scope')));
|
|
}
|
|
$scope = PhabricatorOAuthServerScope::scopesListToDict($scope);
|
|
} else {
|
|
return $this->buildErrorResponse(
|
|
'invalid_request',
|
|
pht('Malformed Request'),
|
|
pht(
|
|
'Required parameter %s was not present in the request.',
|
|
phutil_tag('strong', array(), '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()) {
|
|
$scope = PhabricatorOAuthServerScope::getScopesFromRequest($request);
|
|
|
|
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()) {
|
|
return id(new AphrontRedirectResponse())
|
|
->setIsExternal(true)
|
|
->setURI((string)$full_uri);
|
|
}
|
|
|
|
// 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) {
|
|
$desired_scopes = array_merge($scope, $authorization->getScope());
|
|
} else {
|
|
$desired_scopes = $scope;
|
|
}
|
|
if (!PhabricatorOAuthServerScope::validateScopesDict($desired_scopes)) {
|
|
return $this->buildErrorResponse(
|
|
'invalid_scope',
|
|
pht('Invalid Scope'),
|
|
pht('The requested scope is invalid, unknown, or malformed.'));
|
|
}
|
|
|
|
$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)
|
|
->appendChild(
|
|
PhabricatorOAuthServerScope::getCheckboxControl($desired_scopes));
|
|
|
|
$cancel_msg = pht('The user declined to authorize this application.');
|
|
$cancel_uri = $this->addQueryParams(
|
|
$uri,
|
|
array(
|
|
'error' => 'access_denied',
|
|
'error_description' => $cancel_msg,
|
|
));
|
|
|
|
$dialog = id(new AphrontDialogView())
|
|
->setUser($viewer)
|
|
->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?',
|
|
phutil_tag('strong', array(), $name)))
|
|
->appendChild($form->buildLayoutView())
|
|
->addSubmitButton(pht('Authorize Access'))
|
|
->addCancelButton((string)$cancel_uri, pht('Do Not Authorize'));
|
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
|
}
|
|
|
|
|
|
private function buildErrorResponse($code, $title, $message) {
|
|
$viewer = $this->getRequest()->getUser();
|
|
|
|
$dialog = id(new AphrontDialogView())
|
|
->setUser($viewer)
|
|
->setTitle(pht('OAuth: %s', $title))
|
|
->appendParagraph($message)
|
|
->appendParagraph(
|
|
pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code)))
|
|
->addCancelButton('/', pht('Alas!'));
|
|
|
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
}
|