4 * Implements core OAuth 2.0 Server logic.
6 * This class should be used behind business logic that parses input to
7 * determine pertinent @{class:PhabricatorUser} $user,
8 * @{class:PhabricatorOAuthServerClient} $client(s),
9 * @{class:PhabricatorOAuthServerAuthorizationCode} $code(s), and.
10 * @{class:PhabricatorOAuthServerAccessToken} $token(s).
12 * For an OAuth 2.0 server, there are two main steps:
14 * 1) Authorization - the user authorizes a given client to access the data
15 * the OAuth 2.0 server protects. Once this is achieved / if it has
16 * been achived already, the OAuth server sends the client an authorization
18 * 2) Access Token - the client should send the authorization code received in
19 * step 1 along with its id and secret to the OAuth server to receive an
20 * access token. This access token can later be used to access Phabricator
21 * data on behalf of the user.
23 * @task auth Authorizing @{class:PhabricatorOAuthServerClient}s and
24 * generating @{class:PhabricatorOAuthServerAuthorizationCode}s
25 * @task token Validating @{class:PhabricatorOAuthServerAuthorizationCode}s
26 * and generating @{class:PhabricatorOAuthServerAccessToken}s
27 * @task internal Internals
29 final class PhabricatorOAuthServer
extends Phobject
{
31 const AUTHORIZATION_CODE_TIMEOUT
= 300;
36 private function getUser() {
38 throw new PhutilInvalidStateException('setUser');
43 public function setUser(PhabricatorUser
$user) {
48 private function getClient() {
50 throw new PhutilInvalidStateException('setClient');
55 public function setClient(PhabricatorOAuthServerClient
$client) {
56 $this->client
= $client;
62 * @return tuple <bool hasAuthorized, ClientAuthorization or null>
64 public function userHasAuthorizedClient(array $scope) {
66 $authorization = id(new PhabricatorOAuthClientAuthorization())
68 'userPHID = %s AND clientPHID = %s',
69 $this->getUser()->getPHID(),
70 $this->getClient()->getPHID());
71 if (empty($authorization)) {
72 return array(false, null);
76 $missing_scope = array_diff_key($scope, $authorization->getScope());
78 $missing_scope = false;
82 return array(false, $authorization);
85 return array(true, $authorization);
91 public function authorizeClient(array $scope) {
92 $authorization = new PhabricatorOAuthClientAuthorization();
93 $authorization->setUserPHID($this->getUser()->getPHID());
94 $authorization->setClientPHID($this->getClient()->getPHID());
95 $authorization->setScope($scope);
96 $authorization->save();
98 return $authorization;
104 public function generateAuthorizationCode(PhutilURI
$redirect_uri) {
106 $code = Filesystem
::readRandomCharacters(32);
107 $client = $this->getClient();
109 $authorization_code = new PhabricatorOAuthServerAuthorizationCode();
110 $authorization_code->setCode($code);
111 $authorization_code->setClientPHID($client->getPHID());
112 $authorization_code->setClientSecret($client->getSecret());
113 $authorization_code->setUserPHID($this->getUser()->getPHID());
114 $authorization_code->setRedirectURI((string)$redirect_uri);
115 $authorization_code->save();
117 return $authorization_code;
123 public function generateAccessToken() {
125 $token = Filesystem
::readRandomCharacters(32);
127 $access_token = new PhabricatorOAuthServerAccessToken();
128 $access_token->setToken($token);
129 $access_token->setUserPHID($this->getUser()->getPHID());
130 $access_token->setClientPHID($this->getClient()->getPHID());
131 $access_token->save();
133 return $access_token;
139 public function validateAuthorizationCode(
140 PhabricatorOAuthServerAuthorizationCode
$test_code,
141 PhabricatorOAuthServerAuthorizationCode
$valid_code) {
143 // check that all the meta data matches
144 if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {
147 if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {
151 // check that the authorization code hasn't timed out
152 $created_time = $test_code->getDateCreated();
153 $must_be_used_by = $created_time + self
::AUTHORIZATION_CODE_TIMEOUT
;
154 return (time() < $must_be_used_by);
160 public function authorizeToken(
161 PhabricatorOAuthServerAccessToken
$token) {
163 $user_phid = $token->getUserPHID();
164 $client_phid = $token->getClientPHID();
166 $authorization = id(new PhabricatorOAuthClientAuthorizationQuery())
167 ->setViewer(PhabricatorUser
::getOmnipotentUser())
168 ->withUserPHIDs(array($user_phid))
169 ->withClientPHIDs(array($client_phid))
171 if (!$authorization) {
175 $application = $authorization->getClient();
176 if ($application->getIsDisabled()) {
180 return $authorization;
183 public function validateRedirectURI($uri) {
185 $this->assertValidRedirectURI($uri);
187 } catch (Exception
$ex) {
193 * See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2
194 * for details on what makes a given redirect URI "valid".
196 public function assertValidRedirectURI($raw_uri) {
197 // This covers basics like reasonable formatting and the existence of a
199 PhabricatorEnv
::requireValidRemoteURIForLink($raw_uri);
201 $uri = new PhutilURI($raw_uri);
203 $fragment = $uri->getFragment();
204 if (strlen($fragment)) {
207 'OAuth application redirect URIs must not contain URI '.
208 'fragments, but the URI "%s" has a fragment ("%s").',
213 $protocol = $uri->getProtocol();
221 'OAuth application redirect URIs must only use the "http" or '.
222 '"https" protocols, but the URI "%s" uses the "%s" protocol.',
229 * If there's a URI specified in an OAuth request, it must be validated in
230 * its own right. Further, it must have the same domain, the same path, the
231 * same port, and (at least) the same query parameters as the primary URI.
233 public function validateSecondaryRedirectURI(
234 PhutilURI
$secondary_uri,
235 PhutilURI
$primary_uri) {
237 // The secondary URI must be valid.
238 if (!$this->validateRedirectURI($secondary_uri)) {
242 // Both URIs must point at the same domain.
243 if ($secondary_uri->getDomain() != $primary_uri->getDomain()) {
247 // Both URIs must have the same path
248 if ($secondary_uri->getPath() != $primary_uri->getPath()) {
252 // Both URIs must have the same port
253 if ($secondary_uri->getPort() != $primary_uri->getPort()) {
257 // Any query parameters present in the first URI must be exactly present
258 // in the second URI.
259 $need_params = $primary_uri->getQueryParamsAsMap();
260 $have_params = $secondary_uri->getQueryParamsAsMap();
262 foreach ($need_params as $key => $value) {
263 if (!array_key_exists($key, $have_params)) {
266 if ((string)$have_params[$key] != (string)$value) {
271 // If the first URI is HTTPS, the second URI must also be HTTPS. This
272 // defuses an attack where a third party with control over the network
273 // tricks you into using HTTP to authenticate over a link which is supposed
274 // to be HTTPS only and sniffs all your token cookies.
275 if (strtolower($primary_uri->getProtocol()) == 'https') {
276 if (strtolower($secondary_uri->getProtocol()) != 'https') {