GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( a3174d...5adad1 )
by François
02:55
created

OAuthServer::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 2
eloc 10
nc 2
nop 7
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'],
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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,
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $userInfo is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $userInfo is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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