Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
12242 views
1
<?php
2
3
final class PhabricatorOAuthServerAuthController
4
extends PhabricatorOAuthServerController {
5
6
protected function buildApplicationCrumbs() {
7
// We're specifically not putting an "OAuth Server" application crumb
8
// on the auth pages because it doesn't make sense to send users there.
9
return new PHUICrumbsView();
10
}
11
12
public function handleRequest(AphrontRequest $request) {
13
$viewer = $this->getViewer();
14
15
$server = new PhabricatorOAuthServer();
16
$client_phid = $request->getStr('client_id');
17
$redirect_uri = $request->getStr('redirect_uri');
18
$response_type = $request->getStr('response_type');
19
20
// state is an opaque value the client sent us for their own purposes
21
// we just need to send it right back to them in the response!
22
$state = $request->getStr('state');
23
24
if (!$client_phid) {
25
return $this->buildErrorResponse(
26
'invalid_request',
27
pht('Malformed Request'),
28
pht(
29
'Required parameter %s was not present in the request.',
30
phutil_tag('strong', array(), 'client_id')));
31
}
32
33
// We require that users must be able to see an OAuth application
34
// in order to authorize it. This allows an application's visibility
35
// policy to be used to restrict authorized users.
36
try {
37
$client = id(new PhabricatorOAuthServerClientQuery())
38
->setViewer($viewer)
39
->withPHIDs(array($client_phid))
40
->executeOne();
41
} catch (PhabricatorPolicyException $ex) {
42
$ex->setContext(self::CONTEXT_AUTHORIZE);
43
throw $ex;
44
}
45
46
$server->setUser($viewer);
47
$is_authorized = false;
48
$authorization = null;
49
$uri = null;
50
$name = null;
51
52
// one giant try / catch around all the exciting database stuff so we
53
// can return a 'server_error' response if something goes wrong!
54
try {
55
if (!$client) {
56
return $this->buildErrorResponse(
57
'invalid_request',
58
pht('Invalid Client Application'),
59
pht(
60
'Request parameter %s does not specify a valid client application.',
61
phutil_tag('strong', array(), 'client_id')));
62
}
63
64
if ($client->getIsDisabled()) {
65
return $this->buildErrorResponse(
66
'invalid_request',
67
pht('Application Disabled'),
68
pht(
69
'The %s OAuth application has been disabled.',
70
phutil_tag('strong', array(), 'client_id')));
71
}
72
73
$name = $client->getName();
74
$server->setClient($client);
75
if ($redirect_uri) {
76
$client_uri = new PhutilURI($client->getRedirectURI());
77
$redirect_uri = new PhutilURI($redirect_uri);
78
if (!($server->validateSecondaryRedirectURI($redirect_uri,
79
$client_uri))) {
80
return $this->buildErrorResponse(
81
'invalid_request',
82
pht('Invalid Redirect URI'),
83
pht(
84
'Request parameter %s specifies an invalid redirect URI. '.
85
'The redirect URI must be a fully-qualified domain with no '.
86
'fragments, and must have the same domain and at least '.
87
'the same query parameters as the redirect URI the client '.
88
'registered.',
89
phutil_tag('strong', array(), 'redirect_uri')));
90
}
91
$uri = $redirect_uri;
92
} else {
93
$uri = new PhutilURI($client->getRedirectURI());
94
}
95
96
if (empty($response_type)) {
97
return $this->buildErrorResponse(
98
'invalid_request',
99
pht('Invalid Response Type'),
100
pht(
101
'Required request parameter %s is missing.',
102
phutil_tag('strong', array(), 'response_type')));
103
}
104
105
if ($response_type != 'code') {
106
return $this->buildErrorResponse(
107
'unsupported_response_type',
108
pht('Unsupported Response Type'),
109
pht(
110
'Request parameter %s specifies an unsupported response type. '.
111
'Valid response types are: %s.',
112
phutil_tag('strong', array(), 'response_type'),
113
implode(', ', array('code'))));
114
}
115
116
117
$requested_scope = $request->getStrList('scope');
118
$requested_scope = array_fuse($requested_scope);
119
120
$scope = PhabricatorOAuthServerScope::filterScope($requested_scope);
121
122
// NOTE: We're always requiring a confirmation dialog to redirect.
123
// Partly this is a general defense against redirect attacks, and
124
// partly this shakes off anchors in the URI (which are not shaken
125
// by 302'ing).
126
127
$auth_info = $server->userHasAuthorizedClient($scope);
128
list($is_authorized, $authorization) = $auth_info;
129
130
if ($request->isFormPost()) {
131
if ($authorization) {
132
$authorization->setScope($scope)->save();
133
} else {
134
$authorization = $server->authorizeClient($scope);
135
}
136
137
$is_authorized = true;
138
}
139
} catch (Exception $e) {
140
return $this->buildErrorResponse(
141
'server_error',
142
pht('Server Error'),
143
pht(
144
'The authorization server encountered an unexpected condition '.
145
'which prevented it from fulfilling the request.'));
146
}
147
148
// When we reach this part of the controller, we can be in two states:
149
//
150
// 1. The user has not authorized the application yet. We want to
151
// give them an "Authorize this application?" dialog.
152
// 2. The user has authorized the application. We want to give them
153
// a "Confirm Login" dialog.
154
155
if ($is_authorized) {
156
157
// The second case is simpler, so handle it first. The user either
158
// authorized the application previously, or has just authorized the
159
// application. Show them a confirm dialog with a normal link back to
160
// the application. This shakes anchors from the URI.
161
162
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
163
$auth_code = $server->generateAuthorizationCode($uri);
164
unset($unguarded);
165
166
$full_uri = $this->addQueryParams(
167
$uri,
168
array(
169
'code' => $auth_code->getCode(),
170
'scope' => $authorization->getScopeString(),
171
'state' => $state,
172
));
173
174
if ($client->getIsTrusted()) {
175
// NOTE: See T13099. We currently emit a "Content-Security-Policy"
176
// which includes a narrow "form-action". At the time of writing,
177
// Chrome applies "form-action" to redirects following form submission.
178
179
// This can lead to a situation where a user enters the OAuth workflow
180
// and is prompted for MFA. When they submit an MFA response, the form
181
// can redirect here, and Chrome will block the "Location" redirect.
182
183
// To avoid this, render an interstitial. We only actually need to do
184
// this in Chrome (but do it everywhere for consistency) and only need
185
// to do it if the request is a redirect after a form submission (but
186
// we can't tell if it is or not).
187
188
Javelin::initBehavior(
189
'redirect',
190
array(
191
'uri' => (string)$full_uri,
192
));
193
194
return $this->newDialog()
195
->setTitle(pht('Authenticate: %s', $name))
196
->appendParagraph(
197
pht(
198
'Authorization for "%s" confirmed, redirecting...',
199
phutil_tag('strong', array(), $name)))
200
->addCancelButton((string)$full_uri, pht('Continue'));
201
}
202
203
// TODO: It would be nice to give the user more options here, like
204
// reviewing permissions, canceling the authorization, or aborting
205
// the workflow.
206
207
$dialog = id(new AphrontDialogView())
208
->setUser($viewer)
209
->setTitle(pht('Authenticate: %s', $name))
210
->appendParagraph(
211
pht(
212
'This application ("%s") is authorized to use your %s '.
213
'credentials. Continue to complete the authentication workflow.',
214
phutil_tag('strong', array(), $name),
215
PlatformSymbols::getPlatformServerName()))
216
->addCancelButton((string)$full_uri, pht('Continue to Application'));
217
218
return id(new AphrontDialogResponse())->setDialog($dialog);
219
}
220
221
// Here, we're confirming authorization for the application.
222
if ($authorization) {
223
$missing_scope = array_diff_key($scope, $authorization->getScope());
224
} else {
225
$missing_scope = $scope;
226
}
227
228
$form = id(new AphrontFormView())
229
->addHiddenInput('client_id', $client_phid)
230
->addHiddenInput('redirect_uri', $redirect_uri)
231
->addHiddenInput('response_type', $response_type)
232
->addHiddenInput('state', $state)
233
->addHiddenInput('scope', $request->getStr('scope'))
234
->setUser($viewer);
235
236
$cancel_msg = pht('The user declined to authorize this application.');
237
$cancel_uri = $this->addQueryParams(
238
$uri,
239
array(
240
'error' => 'access_denied',
241
'error_description' => $cancel_msg,
242
));
243
244
$dialog = $this->newDialog()
245
->setShortTitle(pht('Authorize Access'))
246
->setTitle(pht('Authorize "%s"?', $name))
247
->setSubmitURI($request->getRequestURI()->getPath())
248
->setWidth(AphrontDialogView::WIDTH_FORM)
249
->appendParagraph(
250
pht(
251
'Do you want to authorize the external application "%s" to '.
252
'access your %s account data, including your primary '.
253
'email address?',
254
phutil_tag('strong', array(), $name),
255
PlatformSymbols::getPlatformServerName()))
256
->appendForm($form)
257
->addSubmitButton(pht('Authorize Access'))
258
->addCancelButton((string)$cancel_uri, pht('Do Not Authorize'));
259
260
if ($missing_scope) {
261
$dialog->appendParagraph(
262
pht(
263
'This application has requested these additional permissions. '.
264
'Authorizing it will grant it the permissions it requests:'));
265
foreach ($missing_scope as $scope_key => $ignored) {
266
// TODO: Once we introduce more scopes, explain them here.
267
}
268
}
269
270
$unknown_scope = array_diff_key($requested_scope, $scope);
271
if ($unknown_scope) {
272
$dialog->appendParagraph(
273
pht(
274
'This application also requested additional unrecognized '.
275
'permissions. These permissions may have existed in an older '.
276
'version of the software, or may be from a future version of '.
277
'the software. They will not be granted.'));
278
279
$unknown_form = id(new AphrontFormView())
280
->setViewer($viewer)
281
->appendChild(
282
id(new AphrontFormTextControl())
283
->setLabel(pht('Unknown Scope'))
284
->setValue(implode(', ', array_keys($unknown_scope)))
285
->setDisabled(true));
286
287
$dialog->appendForm($unknown_form);
288
}
289
290
return $dialog;
291
}
292
293
294
private function buildErrorResponse($code, $title, $message) {
295
$viewer = $this->getRequest()->getUser();
296
297
return $this->newDialog()
298
->setTitle(pht('OAuth: %s', $title))
299
->appendParagraph($message)
300
->appendParagraph(
301
pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code)))
302
->addCancelButton('/', pht('Alas!'));
303
}
304
305
306
private function addQueryParams(PhutilURI $uri, array $params) {
307
$full_uri = clone $uri;
308
309
foreach ($params as $key => $value) {
310
if (strlen($value)) {
311
$full_uri->replaceQueryParam($key, $value);
312
}
313
}
314
315
return $full_uri;
316
}
317
318
}
319
320