1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Copyright 2015 François Kooman <[email protected]>. |
5
|
|
|
* |
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
7
|
|
|
* you may not use this file except in compliance with the License. |
8
|
|
|
* You may obtain a copy of the License at |
9
|
|
|
* |
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
11
|
|
|
* |
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
14
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15
|
|
|
* See the License for the specific language governing permissions and |
16
|
|
|
* limitations under the License. |
17
|
|
|
*/ |
18
|
|
|
|
19
|
|
|
namespace fkooman\RemoteStorage\OAuth; |
20
|
|
|
|
21
|
|
|
use fkooman\Http\Exception\BadRequestException; |
22
|
|
|
use fkooman\Http\Exception\UnauthorizedException; |
23
|
|
|
use fkooman\Http\JsonResponse; |
24
|
|
|
use fkooman\Http\RedirectResponse; |
25
|
|
|
use fkooman\Http\Request; |
26
|
|
|
use fkooman\IO\IO; |
27
|
|
|
use fkooman\Rest\Plugin\Authentication\UserInfoInterface; |
28
|
|
|
|
29
|
|
|
class OAuthServer |
30
|
|
|
{ |
31
|
|
|
/** @var ClientStorageInterface */ |
32
|
|
|
private $clientStorage; |
33
|
|
|
|
34
|
|
|
/** @var ResourceServerStorageInterface */ |
35
|
|
|
private $resourceServerStorage; |
36
|
|
|
|
37
|
|
|
/** @var ApprovalStorageInterface */ |
38
|
|
|
private $approvalStorage; |
39
|
|
|
|
40
|
|
|
/** @var AuthorizationCodeStorageInterface */ |
41
|
|
|
private $authorizationCodeStorage; |
42
|
|
|
|
43
|
|
|
/** @var AccessTokenStorageInterface */ |
44
|
|
|
private $accessTokenStorage; |
45
|
|
|
|
46
|
|
|
/** @var array */ |
47
|
|
|
private $options = [ |
48
|
|
|
'require_state' => true, |
49
|
|
|
]; |
50
|
|
|
|
51
|
|
|
/** @var \fkooman\IO\IO */ |
52
|
|
|
private $io; |
53
|
|
|
|
54
|
|
|
public function __construct(ClientStorageInterface $clientStorage, ResourceServerStorageInterface $resourceServerStorage, ApprovalStorageInterface $approvalStorage, AuthorizationCodeStorageInterface $authorizationCodeStorage, AccessTokenStorageInterface $accessTokenStorage, array $options = [], IO $io = null) |
55
|
|
|
{ |
56
|
|
|
$this->clientStorage = $clientStorage; |
57
|
|
|
$this->resourceServerStorage = $resourceServerStorage; |
58
|
|
|
$this->approvalStorage = $approvalStorage; |
59
|
|
|
$this->authorizationCodeStorage = $authorizationCodeStorage; |
60
|
|
|
$this->accessTokenStorage = $accessTokenStorage; |
61
|
|
|
if (null === $io) { |
62
|
|
|
$io = new IO(); |
63
|
|
|
} |
64
|
|
|
$this->options = array_merge($this->options, $options); |
65
|
|
|
$this->io = $io; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
public function getAuthorize(Request $request, UserInfoInterface $userInfo) |
69
|
|
|
{ |
70
|
|
|
$authorizeRequest = RequestValidation::validateAuthorizeRequest($request, $this->options['require_state']); |
71
|
|
|
|
72
|
|
|
$client = $this->clientStorage->getClient( |
73
|
|
|
$authorizeRequest['client_id'], |
74
|
|
|
$authorizeRequest['response_type'], |
75
|
|
|
$authorizeRequest['redirect_uri'], |
76
|
|
|
$authorizeRequest['scope'] |
77
|
|
|
); |
78
|
|
|
if (false === $client) { |
79
|
|
|
throw new BadRequestException('client does not exist'); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
// verify authorize request with client information |
83
|
|
|
$this->validateAuthorizeRequestWithClient($client, $authorizeRequest); |
84
|
|
|
|
85
|
|
|
// if approval is already there, return redirect |
86
|
|
|
$approval = new Approval( |
87
|
|
|
$userInfo->getUserId(), |
88
|
|
|
$client->getClientId(), |
89
|
|
|
$authorizeRequest['response_type'], |
90
|
|
|
$authorizeRequest['scope'] |
91
|
|
|
); |
92
|
|
|
|
93
|
|
|
if ($this->approvalStorage->isApproved($approval)) { |
94
|
|
|
// already approved |
95
|
|
|
return $this->handleApproval($client, $authorizeRequest, $userInfo); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
// if not, show the approval dialog |
99
|
|
|
return [ |
100
|
|
|
'user_id' => $userInfo->getUserId(), |
101
|
|
|
'client_id' => $client->getClientId(), |
102
|
|
|
'redirect_uri' => $authorizeRequest['redirect_uri'], |
103
|
|
|
// 'response_type' => $authorizeRequest['response_type'], |
|
|
|
|
104
|
|
|
'scope' => $authorizeRequest['scope'], |
105
|
|
|
'request_url' => $request->getUrl()->toString(), |
106
|
|
|
]; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
public function postAuthorize(Request $request, UserInfoInterface $userInfo) |
110
|
|
|
{ |
111
|
|
|
// FIXME: referrer url MUST be exact request URL? |
112
|
|
|
$postAuthorizeRequest = RequestValidation::validatePostAuthorizeRequest($request, $this->options['require_state']); |
113
|
|
|
|
114
|
|
|
$client = $this->clientStorage->getClient( |
115
|
|
|
$postAuthorizeRequest['client_id'], |
116
|
|
|
$postAuthorizeRequest['response_type'], |
117
|
|
|
$postAuthorizeRequest['redirect_uri'], |
118
|
|
|
$postAuthorizeRequest['scope'] |
119
|
|
|
); |
120
|
|
|
if (false === $client) { |
121
|
|
|
throw new BadRequestException('client does not exist'); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
// verify authorize request with client information |
125
|
|
|
$this->validateAuthorizeRequestWithClient($client, $postAuthorizeRequest); |
126
|
|
|
|
127
|
|
|
if ('yes' === $postAuthorizeRequest['approval']) { |
128
|
|
|
return $this->handleApproval($client, $postAuthorizeRequest, $userInfo); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
return $this->handleDenial($postAuthorizeRequest, $userInfo); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
public function postToken(Request $request, UserInfoInterface $clientUserInfo = null) |
135
|
|
|
{ |
136
|
|
|
// FIXME: deal with not authenticated attempts! check if the client is |
137
|
|
|
// 'public/anonymous' or not, we have to deny hard here! check client_id |
138
|
|
|
// post parameter, check userInfo->getUserId to match it with client_id |
139
|
|
|
// etc. |
140
|
|
|
|
141
|
|
|
$tokenRequest = RequestValidation::validateTokenRequest($request); |
142
|
|
|
|
143
|
|
|
$client = $this->clientStorage->getClient( |
144
|
|
|
$tokenRequest['client_id'] |
145
|
|
|
); |
146
|
|
|
if (null === $clientUserInfo) { |
147
|
|
|
// unauthenticated client |
148
|
|
|
if (null !== $client->getSecret()) { |
149
|
|
|
// if this is not null, authentication was actually required, but there was no attempt |
150
|
|
|
$e = new UnauthorizedException('not_authenticated', 'client authentication required for this client'); |
151
|
|
|
$e->addScheme( |
152
|
|
|
'Basic', |
153
|
|
|
[ |
154
|
|
|
'realm' => 'OAuth AS', |
155
|
|
|
] |
156
|
|
|
); |
157
|
|
|
throw $e; |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
if (null !== $clientUserInfo) { |
162
|
|
|
// if authenticated, client_id must match the authenticated user |
163
|
|
|
if ($clientUserInfo->getUserId() !== $tokenRequest['client_id']) { |
164
|
|
|
throw new BadRequestException('client_id does not match authenticated user'); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
// check code was not used before |
169
|
|
|
if (!$this->authorizationCodeStorage->isFreshAuthorizationCode($tokenRequest['code'])) { |
170
|
|
|
throw new BadRequestException('authorization code can not be replayed'); |
171
|
|
|
} |
172
|
|
|
$authorizationCode = $this->authorizationCodeStorage->retrieveAuthorizationCode($tokenRequest['code']); |
173
|
|
|
|
174
|
|
|
$issuedAt = $authorizationCode->getIssuedAt(); |
175
|
|
|
if ($this->io->getTime() > $issuedAt + 600) { |
176
|
|
|
throw new BadRequestException('authorization code expired'); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
if ($authorizationCode->getClientId() !== $tokenRequest['client_id']) { |
180
|
|
|
throw new BadRequestException('client_id does not match expected value'); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
if (null !== $authorizationCode->getRedirectUri()) { |
184
|
|
|
if ($authorizationCode->getRedirectUri() !== $tokenRequest['redirect_uri']) { |
185
|
|
|
throw new BadRequestException('redirect_uri does not match expected value'); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
// FIXME: grant_type must also match I think, but we do not have any |
190
|
|
|
// mapping logic from response_type to grant_type yet... |
191
|
|
|
|
192
|
|
|
// create an access token |
193
|
|
|
$accessToken = $this->accessTokenStorage->storeAccessToken( |
194
|
|
|
new AccessToken( |
195
|
|
|
$authorizationCode->getClientId(), |
196
|
|
|
$authorizationCode->getUserId(), |
197
|
|
|
$this->io->getTime(), |
198
|
|
|
$authorizationCode->getScope() |
199
|
|
|
) |
200
|
|
|
); |
201
|
|
|
|
202
|
|
|
$response = new JsonResponse(); |
203
|
|
|
$response->setHeader('Cache-Control', 'no-store'); |
204
|
|
|
$response->setHeader('Pragma', 'no-cache'); |
205
|
|
|
$response->setBody( |
206
|
|
|
[ |
207
|
|
|
'access_token' => $accessToken, |
208
|
|
|
'scope' => $authorizationCode->getScope(), |
209
|
|
|
'token_type' => 'bearer', |
210
|
|
|
//'expires_in' => 3600, |
|
|
|
|
211
|
|
|
] |
212
|
|
|
); |
213
|
|
|
|
214
|
|
|
return $response; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
public function postIntrospect(Request $request, UserInfoInterface $userInfo) |
218
|
|
|
{ |
219
|
|
|
$introspectRequest = RequestValidation::validateIntrospectRequest($request); |
220
|
|
|
$accessToken = $this->accessTokenStorage->retrieveAccessToken($introspectRequest['token']); |
221
|
|
|
|
222
|
|
|
if (false === $accessToken) { |
223
|
|
|
$body = [ |
224
|
|
|
'active' => false, |
225
|
|
|
]; |
226
|
|
|
} else { |
227
|
|
|
$resourceServerInfo = $this->resourceServerStorage->getResourceServer($userInfo->getUserId()); |
228
|
|
|
$resourceServerScope = new Scope($resourceServerInfo->getScope()); |
229
|
|
|
$accessTokenScope = new Scope($accessToken->getScope()); |
230
|
|
|
// the scopes from the access token needs to be supported by the |
231
|
|
|
// resource server, otherwise the token is not valid (for this |
232
|
|
|
// resource server, i.e. audience mismatch) |
233
|
|
|
|
234
|
|
|
if (!$resourceServerScope->hasScope($accessTokenScope)) { |
235
|
|
|
$body = [ |
236
|
|
|
'active' => false, |
237
|
|
|
]; |
238
|
|
|
} else { |
239
|
|
|
$body = [ |
240
|
|
|
'active' => true, |
241
|
|
|
'client_id' => $accessToken->getClientId(), |
242
|
|
|
'scope' => $accessToken->getScope(), |
243
|
|
|
'token_type' => 'bearer', |
244
|
|
|
'iat' => $accessToken->getIssuedAt(), |
245
|
|
|
'sub' => $accessToken->getUserId(), |
246
|
|
|
]; |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$response = new JsonResponse(); |
251
|
|
|
$response->setBody($body); |
252
|
|
|
|
253
|
|
|
return $response; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
private function validateAuthorizeRequestWithClient(Client $client, array $authorizeRequest) |
257
|
|
|
{ |
258
|
|
|
if ($client->getResponseType() !== $authorizeRequest['response_type']) { |
259
|
|
|
throw new BadRequestException('response_type not supported by client'); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
if (null !== $authorizeRequest['redirect_uri']) { |
263
|
|
|
if ($client->getRedirectUri() !== $authorizeRequest['redirect_uri']) { |
264
|
|
|
throw new BadRequestException('redirect_uri not supported by client'); |
265
|
|
|
} |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
if (null !== $authorizeRequest['scope']) { |
269
|
|
|
$requestScope = new Scope($authorizeRequest['scope']); |
270
|
|
|
$clientScope = new Scope($client->getScope()); |
271
|
|
|
if (!$clientScope->hasScope($requestScope)) { |
272
|
|
|
throw new BadRequestException('scope not supported by client'); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
private function handleApproval(Client $client, array $postAuthorizeRequest, UserInfoInterface $userInfo) |
278
|
|
|
{ |
279
|
|
|
// store the approval if not yet approved |
280
|
|
|
$approval = new Approval( |
281
|
|
|
$userInfo->getUserId(), |
282
|
|
|
$client->getClientId(), |
283
|
|
|
$postAuthorizeRequest['response_type'], |
284
|
|
|
$postAuthorizeRequest['scope'] |
285
|
|
|
); |
286
|
|
|
|
287
|
|
|
if (!$this->approvalStorage->isApproved($approval)) { |
288
|
|
|
$this->approvalStorage->storeApproval($approval); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
View Code Duplication |
switch ($postAuthorizeRequest['response_type']) { |
|
|
|
|
292
|
|
|
case 'code': |
293
|
|
|
return $this->handleCodeApproval($client, $postAuthorizeRequest, $userInfo); |
294
|
|
|
case 'token': |
295
|
|
|
return $this->handleTokenApproval($client, $postAuthorizeRequest, $userInfo); |
296
|
|
|
default: |
297
|
|
|
throw new BadRequestException('invalid response_type'); |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
private function handleDenial(array $postAuthorizeRequest, UserInfoInterface $userInfo) |
302
|
|
|
{ |
303
|
|
View Code Duplication |
switch ($postAuthorizeRequest['response_type']) { |
|
|
|
|
304
|
|
|
case 'code': |
305
|
|
|
return $this->handleCodeDenial($postAuthorizeRequest, $userInfo); |
306
|
|
|
case 'token': |
307
|
|
|
return $this->handleTokenDenial($postAuthorizeRequest, $userInfo); |
308
|
|
|
default: |
309
|
|
|
throw new BadRequestException('invalid response_type'); |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
private function handleCodeApproval(Client $client, array $postAuthorizeRequest, UserInfoInterface $userInfo) |
314
|
|
|
{ |
315
|
|
|
// generate authorization code and redirect back to client |
316
|
|
|
$code = $this->authorizationCodeStorage->storeAuthorizationCode( |
317
|
|
|
new AuthorizationCode( |
318
|
|
|
$client->getClientId(), |
319
|
|
|
$userInfo->getUserId(), |
320
|
|
|
$this->io->getTime(), |
321
|
|
|
$postAuthorizeRequest['redirect_uri'], |
322
|
|
|
$postAuthorizeRequest['scope'] |
323
|
|
|
) |
324
|
|
|
); |
325
|
|
|
|
326
|
|
|
$separator = false === strpos($postAuthorizeRequest['redirect_uri'], '?') ? '?' : '&'; |
327
|
|
|
|
328
|
|
|
$redirectTo = sprintf( |
329
|
|
|
'%s%scode=%s&state=%s', |
330
|
|
|
$postAuthorizeRequest['redirect_uri'], |
331
|
|
|
$separator, |
332
|
|
|
$code, |
333
|
|
|
$postAuthorizeRequest['state'] |
334
|
|
|
); |
335
|
|
|
|
336
|
|
|
return new RedirectResponse( |
337
|
|
|
$redirectTo, |
338
|
|
|
302 |
339
|
|
|
); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
private function handleCodeDenial(array $postAuthorizeRequest, UserInfoInterface $userInfo) |
|
|
|
|
343
|
|
|
{ |
344
|
|
|
$separator = false === strpos($postAuthorizeRequest['redirect_uri'], '?') ? '?' : '&'; |
345
|
|
|
|
346
|
|
|
$redirectTo = sprintf( |
347
|
|
|
'%s%serror=access_denied&state=%s', |
348
|
|
|
$postAuthorizeRequest['redirect_uri'], |
349
|
|
|
$separator, |
350
|
|
|
$postAuthorizeRequest['state'] |
351
|
|
|
); |
352
|
|
|
|
353
|
|
|
return new RedirectResponse( |
354
|
|
|
$redirectTo, |
355
|
|
|
302 |
356
|
|
|
); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
private function handleTokenApproval(Client $client, array $postAuthorizeRequest, UserInfoInterface $userInfo) |
360
|
|
|
{ |
361
|
|
|
// generate access token and redirect back to client |
362
|
|
|
$accessToken = $this->accessTokenStorage->storeAccessToken( |
363
|
|
|
new AccessToken( |
364
|
|
|
$client->getClientId(), |
365
|
|
|
$userInfo->getUserId(), |
366
|
|
|
$this->io->getTime(), |
367
|
|
|
$postAuthorizeRequest['scope'] |
368
|
|
|
) |
369
|
|
|
); |
370
|
|
|
|
371
|
|
|
// InputValidation already checks that the redirect_uri does not have |
372
|
|
|
// a fragment... |
373
|
|
|
$redirectTo = sprintf( |
374
|
|
|
'%s#access_token=%s&token_type=bearer&state=%s', |
375
|
|
|
$postAuthorizeRequest['redirect_uri'], |
376
|
|
|
$accessToken, |
377
|
|
|
$postAuthorizeRequest['state'] |
378
|
|
|
); |
379
|
|
|
|
380
|
|
|
return new RedirectResponse( |
381
|
|
|
$redirectTo, |
382
|
|
|
302 |
383
|
|
|
); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
private function handleTokenDenial(array $postAuthorizeRequest, UserInfoInterface $userInfo) |
|
|
|
|
387
|
|
|
{ |
388
|
|
|
// InputValidation already checks that the redirect_uri does not have |
389
|
|
|
// a fragment... |
390
|
|
|
$redirectTo = sprintf( |
391
|
|
|
'%s#error=access_denied&state=%s', |
392
|
|
|
$postAuthorizeRequest['redirect_uri'], |
393
|
|
|
$postAuthorizeRequest['state'] |
394
|
|
|
); |
395
|
|
|
|
396
|
|
|
return new RedirectResponse( |
397
|
|
|
$redirectTo, |
398
|
|
|
302 |
399
|
|
|
); |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.