@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Implements core OAuth 2.0 Server logic.
5 *
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).
11 *
12 * For an OAuth 2.0 server, there are two main steps:
13 *
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
17 * code.
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.
22 *
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
28 */
29final class PhabricatorOAuthServer extends Phobject {
30
31 const AUTHORIZATION_CODE_TIMEOUT = 300;
32
33 private $user;
34 private $client;
35
36 private function getUser() {
37 if (!$this->user) {
38 throw new PhutilInvalidStateException('setUser');
39 }
40 return $this->user;
41 }
42
43 public function setUser(PhabricatorUser $user) {
44 $this->user = $user;
45 return $this;
46 }
47
48 private function getClient() {
49 if (!$this->client) {
50 throw new PhutilInvalidStateException('setClient');
51 }
52 return $this->client;
53 }
54
55 public function setClient(PhabricatorOAuthServerClient $client) {
56 $this->client = $client;
57 return $this;
58 }
59
60 /**
61 * @task auth
62 * @return array<bool,PhabricatorOAuthClientAuthorization|null> Tuple of
63 * <bool hasAuthorized, ClientAuthorization or null>
64 */
65 public function userHasAuthorizedClient(array $scope) {
66
67 $authorization = id(new PhabricatorOAuthClientAuthorization())
68 ->loadOneWhere(
69 'userPHID = %s AND clientPHID = %s',
70 $this->getUser()->getPHID(),
71 $this->getClient()->getPHID());
72 if (empty($authorization)) {
73 return array(false, null);
74 }
75
76 if ($scope) {
77 $missing_scope = array_diff_key($scope, $authorization->getScope());
78 } else {
79 $missing_scope = false;
80 }
81
82 if ($missing_scope) {
83 return array(false, $authorization);
84 }
85
86 return array(true, $authorization);
87 }
88
89 /**
90 * @task auth
91 */
92 public function authorizeClient(array $scope) {
93 $authorization = new PhabricatorOAuthClientAuthorization();
94 $authorization->setUserPHID($this->getUser()->getPHID());
95 $authorization->setClientPHID($this->getClient()->getPHID());
96 $authorization->setScope($scope);
97 $authorization->save();
98
99 return $authorization;
100 }
101
102 /**
103 * @task auth
104 */
105 public function generateAuthorizationCode(PhutilURI $redirect_uri) {
106
107 $code = Filesystem::readRandomCharacters(32);
108 $client = $this->getClient();
109
110 $authorization_code = new PhabricatorOAuthServerAuthorizationCode();
111 $authorization_code->setCode($code);
112 $authorization_code->setClientPHID($client->getPHID());
113 $authorization_code->setClientSecret($client->getSecret());
114 $authorization_code->setUserPHID($this->getUser()->getPHID());
115 $authorization_code->setRedirectURI((string)$redirect_uri);
116 $authorization_code->save();
117
118 return $authorization_code;
119 }
120
121 /**
122 * @task token
123 */
124 public function generateAccessToken() {
125
126 $token = Filesystem::readRandomCharacters(32);
127
128 $access_token = new PhabricatorOAuthServerAccessToken();
129 $access_token->setToken($token);
130 $access_token->setUserPHID($this->getUser()->getPHID());
131 $access_token->setClientPHID($this->getClient()->getPHID());
132 $access_token->save();
133
134 return $access_token;
135 }
136
137 /**
138 * @task token
139 */
140 public function validateAuthorizationCode(
141 PhabricatorOAuthServerAuthorizationCode $test_code,
142 PhabricatorOAuthServerAuthorizationCode $valid_code) {
143
144 // check that all the meta data matches
145 if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {
146 return false;
147 }
148 if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {
149 return false;
150 }
151
152 // check that the authorization code hasn't timed out
153 $created_time = $test_code->getDateCreated();
154 $must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT;
155 return (time() < $must_be_used_by);
156 }
157
158 /**
159 * @task token
160 */
161 public function authorizeToken(
162 PhabricatorOAuthServerAccessToken $token) {
163
164 $user_phid = $token->getUserPHID();
165 $client_phid = $token->getClientPHID();
166
167 $authorization = id(new PhabricatorOAuthClientAuthorizationQuery())
168 ->setViewer(PhabricatorUser::getOmnipotentUser())
169 ->withUserPHIDs(array($user_phid))
170 ->withClientPHIDs(array($client_phid))
171 ->executeOne();
172 if (!$authorization) {
173 return null;
174 }
175
176 $application = $authorization->getClient();
177 if ($application->getIsDisabled()) {
178 return null;
179 }
180
181 return $authorization;
182 }
183
184 public function validateRedirectURI($uri) {
185 try {
186 $this->assertValidRedirectURI($uri);
187 return true;
188 } catch (Exception $ex) {
189 return false;
190 }
191 }
192
193 /**
194 * See https://datatracker.ietf.org/doc/rfc6749/ section 3.1.2
195 * for details on what makes a given redirect URI "valid".
196 */
197 public function assertValidRedirectURI($raw_uri) {
198 // This covers basics like reasonable formatting and the existence of a
199 // protocol.
200 PhabricatorEnv::requireValidRemoteURIForLink($raw_uri);
201
202 $uri = new PhutilURI($raw_uri);
203
204 $fragment = $uri->getFragment();
205 if (strlen($fragment)) {
206 throw new Exception(
207 pht(
208 'OAuth application redirect URIs must not contain URI '.
209 'fragments, but the URI "%s" has a fragment ("%s").',
210 $raw_uri,
211 $fragment));
212 }
213
214 $protocol = $uri->getProtocol();
215 switch ($protocol) {
216 case 'http':
217 case 'https':
218 break;
219 default:
220 throw new Exception(
221 pht(
222 'OAuth application redirect URIs must only use the "http" or '.
223 '"https" protocols, but the URI "%s" uses the "%s" protocol.',
224 $raw_uri,
225 $protocol));
226 }
227 }
228
229 /**
230 * If there's a URI specified in an OAuth request, it must be validated in
231 * its own right. Further, it must have the same domain, the same path, the
232 * same port, and (at least) the same query parameters as the primary URI.
233 */
234 public function validateSecondaryRedirectURI(
235 PhutilURI $secondary_uri,
236 PhutilURI $primary_uri) {
237
238 // The secondary URI must be valid.
239 if (!$this->validateRedirectURI($secondary_uri)) {
240 return false;
241 }
242
243 // Both URIs must point at the same domain.
244 if ($secondary_uri->getDomain() != $primary_uri->getDomain()) {
245 return false;
246 }
247
248 // Both URIs must have the same path
249 if ($secondary_uri->getPath() != $primary_uri->getPath()) {
250 return false;
251 }
252
253 // Both URIs must have the same port
254 if ($secondary_uri->getPort() != $primary_uri->getPort()) {
255 return false;
256 }
257
258 // Any query parameters present in the first URI must be exactly present
259 // in the second URI.
260 $need_params = $primary_uri->getQueryParamsAsMap();
261 $have_params = $secondary_uri->getQueryParamsAsMap();
262
263 foreach ($need_params as $key => $value) {
264 if (!array_key_exists($key, $have_params)) {
265 return false;
266 }
267 if ((string)$have_params[$key] != (string)$value) {
268 return false;
269 }
270 }
271
272 // If the first URI is HTTPS, the second URI must also be HTTPS. This
273 // defuses an attack where a third party with control over the network
274 // tricks you into using HTTP to authenticate over a link which is supposed
275 // to be HTTPS only and sniffs all your token cookies.
276 if (strtolower($primary_uri->getProtocol()) == 'https') {
277 if (strtolower($secondary_uri->getProtocol()) != 'https') {
278 return false;
279 }
280 }
281
282 return true;
283 }
284
285}