Passed
Push — develop ( 509a81...899aca )
by nguereza
02:24
created

AuthorizationServer::createResponseFromException()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine OAuth2
5
 *
6
 * Platine OAuth2 is a library that implements the OAuth2 specification
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine OAuth2
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
declare(strict_types=1);
32
33
namespace Platine\OAuth2;
34
35
use Platine\Http\Response;
36
use Platine\Http\ResponseInterface;
37
use Platine\Http\ServerRequestInterface;
38
use Platine\Logger\LoggerInterface;
39
use Platine\OAuth2\AuthorizationServerInterface;
40
use Platine\OAuth2\Entity\Client;
41
use Platine\OAuth2\Entity\TokenOwnerInterface;
42
use Platine\OAuth2\Exception\OAuth2Exception;
43
use Platine\OAuth2\Grant\AuthorizationServerAwareInterface;
44
use Platine\OAuth2\Grant\GrantInterface;
45
use Platine\OAuth2\Response\JsonResponse;
46
use Platine\OAuth2\Service\AccessTokenService;
47
use Platine\OAuth2\Service\ClientService;
48
use Platine\OAuth2\Service\RefreshTokenService;
49
use Throwable;
50
51
/**
52
 * @class AuthorizationServer
53
 * @package Platine\OAuth2
54
 */
55
class AuthorizationServer implements AuthorizationServerInterface
56
{
57
    /**
58
     * The ClientService
59
     * @var ClientService
60
     */
61
    protected ClientService $clientService;
62
63
    /**
64
     * The AccessTokenService
65
     * @var AccessTokenService
66
     */
67
    protected AccessTokenService $accessTokenService;
68
69
    /**
70
     * The RefreshTokenService
71
     * @var RefreshTokenService
72
     */
73
    protected RefreshTokenService $refreshTokenService;
74
75
    /**
76
     * The logger instance
77
     * @var LoggerInterface
78
     */
79
    protected LoggerInterface $logger;
80
81
82
    /**
83
     * The grant list
84
     * @var array<string, GrantInterface>
85
     */
86
    protected array $grants = [];
87
88
    /**
89
     * A list of grant that can answer to an authorization request
90
     * @var array<string, GrantInterface>
91
     */
92
    protected array $responseTypes = [];
93
94
    /**
95
     * Create new instance
96
     * @param ClientService $clientService
97
     * @param AccessTokenService $accessTokenService
98
     * @param RefreshTokenService $refreshTokenService
99
     * @param LoggerInterface $logger
100
     * @param array<GrantInterface> $grants
101
     */
102
    public function __construct(
103
        ClientService $clientService,
104
        AccessTokenService $accessTokenService,
105
        RefreshTokenService $refreshTokenService,
106
        LoggerInterface $logger,
107
        array $grants = []
108
    ) {
109
        $this->clientService = $clientService;
110
        $this->accessTokenService = $accessTokenService;
111
        $this->refreshTokenService = $refreshTokenService;
112
        $this->logger = $logger;
113
114
        foreach ($grants as /** @var GrantInterface $grant */ $grant) {
115
            if ($grant instanceof AuthorizationServerAwareInterface) {
116
                $grant->setAuthorizationServer($this);
117
            }
118
119
            $this->grants[$grant->getType()] = $grant;
0 ignored issues
show
Bug introduced by
The method getType() does not exist on Platine\OAuth2\Grant\Aut...ionServerAwareInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Platine\OAuth2\Grant\Aut...ionServerAwareInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

119
            $this->grants[$grant->/** @scrutinizer ignore-call */ getType()] = $grant;
Loading history...
120
121
            $responseType = $grant->getResponseType();
0 ignored issues
show
Bug introduced by
The method getResponseType() does not exist on Platine\OAuth2\Grant\Aut...ionServerAwareInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Platine\OAuth2\Grant\Aut...ionServerAwareInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

121
            /** @scrutinizer ignore-call */ 
122
            $responseType = $grant->getResponseType();
Loading history...
122
            if (!empty($responseType)) {
123
                $this->responseTypes[$responseType] = $grant;
124
            }
125
        }
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function handleAuthorizationRequest(
132
        ServerRequestInterface $request,
133
        ?TokenOwnerInterface $owner = null
134
    ): ResponseInterface {
135
        try {
136
            $queryParams = $request->getQueryParams();
137
            $responseTypeParam = $queryParams['response_type'] ?? null;
138
            if ($responseTypeParam === null) {
139
                throw OAuth2Exception::invalidRequest('No grant response type was found in the request');
140
            }
141
142
            $responseType = $this->getResponseType((string) $responseTypeParam);
143
            $client = $this->getClient($request, $responseType->allowPublicClients());
144
145
            if ($client === null) {
146
                throw OAuth2Exception::invalidClient('No client could be authenticated');
147
            }
148
            $response = $responseType->createAuthorizationResponse($request, $client, $owner);
149
        } catch (OAuth2Exception $ex) {
150
            $response = $this->createResponseFromException($ex);
151
        }
152
153
        return $response->withHeader('Content-Type', 'application/json');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...e', 'application/json') could return the type Platine\Http\MessageInterface which includes types incompatible with the type-hinted return Platine\Http\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function handleTokenRequest(
160
        ServerRequestInterface $request,
161
        ?TokenOwnerInterface $owner = null
162
    ): ResponseInterface {
163
        $postParams = (array) $request->getParsedBody();
164
165
        try {
166
            $grantParam = $postParams['grant_type'] ?? null;
167
            if ($grantParam === null) {
168
                throw OAuth2Exception::invalidRequest('No grant type was found in the request');
169
            }
170
171
            $grant = $this->getGrant((string) $grantParam);
172
            $client = $this->getClient($request, $grant->allowPublicClients());
173
174
            if ($client === null) {
175
                throw OAuth2Exception::invalidClient('No client could be authenticated');
176
            }
177
178
            $response = $grant->createTokenResponse($request, $client, $owner);
179
        } catch (OAuth2Exception $ex) {
180
            $response = $this->createResponseFromException($ex);
181
        }
182
183
        // According to the spec, we must set those headers
184
        // (http://tools.ietf.org/html/rfc6749#section-5.1)
185
        return $response->withHeader('Content-Type', 'application/json')
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...r('Pragma', 'no-cache') could return the type Platine\Http\MessageInterface which includes types incompatible with the type-hinted return Platine\Http\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
186
                        ->withHeader('Cache-Control', 'no-store')
187
                        ->withHeader('Pragma', 'no-cache');
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    public function handleTokenRevocationRequest(ServerRequestInterface $request): ResponseInterface
194
    {
195
        $response = new Response();
196
        try {
197
            $postParams = (array) $request->getParsedBody();
198
            $tokenParam = $postParams['token'] ?? null;
199
            $tokenHint = $postParams['token_type_hint'] ?? null;
200
            if ($tokenParam === null || $tokenHint === null) {
201
                throw OAuth2Exception::invalidRequest(
202
                    'Cannot revoke a token as the "token" and/or "token_type_hint" parameters are missing'
203
                );
204
            }
205
206
            if (in_array($tokenHint, ['access_token', 'refresh_token']) === false) {
207
                throw OAuth2Exception::unsupportedTokenType(sprintf(
208
                    'Authorization server does not support revocation of token of type "%s"',
209
                    $tokenHint
210
                ));
211
            }
212
213
            if ($tokenHint === 'access_token') {
214
                $token = $this->accessTokenService->getToken((string) $tokenParam);
215
            } else {
216
                $token = $this->refreshTokenService->getToken((string) $tokenParam);
217
            }
218
219
            // According to spec, we should return 200 if token is invalid
220
            if ($token === null) {
221
                return $response;
222
            }
223
224
            // Now, we must validate the client if the token was generated against a non-public client
225
            if ($token->getClient() !== null && $token->getClient()->isPublic() === false) {
226
                $requestClient = $this->getClient($request, false);
227
228
                if ($requestClient !== null && $requestClient->getId() !== $token->getClient()->getId()) {
229
                    throw OAuth2Exception::invalidClient(
230
                        'Token was issued for another client and cannot be revoked'
231
                    );
232
                }
233
            }
234
235
            if ($tokenHint === 'access_token') {
236
                $this->accessTokenService->delete($token);
237
            } else {
238
                $this->refreshTokenService->delete($token);
239
            }
240
        } catch (OAuth2Exception $exception) {
241
            $response = $this->createResponseFromException($exception);
242
        } catch (Throwable $exception) {
243
            // According to spec (https://tools.ietf.org/html/rfc7009#section-2.2.1),
244
            // we should return a server 503
245
            // error if we cannot delete the token for any reason
246
            $response = $response->withStatus(503, 'An error occurred while trying to delete the token');
247
248
            $this->logger->error('OAuth2 Error when revoke token: {type}::{code}:{description}', [
249
                'code' => $exception->getCode(),
250
                'description' => $exception->getMessage(),
251
                'type' => get_class($exception),
252
            ]);
253
        }
254
255
        return $response;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function hasGrant(string $grant): bool
262
    {
263
        return array_key_exists($grant, $this->grants);
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function hasResponseType(string $responseType): bool
270
    {
271
        return array_key_exists($responseType, $this->responseTypes);
272
    }
273
274
    /**
275
     * Return the grant
276
     * @param string $grantType
277
     * @return GrantInterface
278
     */
279
    public function getGrant(string $grantType): GrantInterface
280
    {
281
        if ($this->hasGrant($grantType)) {
282
            return $this->grants[$grantType];
283
        }
284
285
        throw OAuth2Exception::unsupportedGrantType(sprintf(
286
            'Grant type "%s" is not supported by this server',
287
            $grantType
288
        ));
289
    }
290
291
    /**
292
     * Return the grant response type
293
     * @param string $responseType
294
     * @return GrantInterface
295
     */
296
    public function getResponseType(string $responseType): GrantInterface
297
    {
298
        if ($this->hasResponseType($responseType)) {
299
            return $this->responseTypes[$responseType];
300
        }
301
302
        throw OAuth2Exception::unsupportedResponseType(sprintf(
303
            'Response type "%s" is not supported by this server',
304
            $responseType
305
        ));
306
    }
307
308
    /**
309
     * Get the client (after authenticating it)
310
     *
311
     * According to the spec (http://tools.ietf.org/html/rfc6749#section-2.3), for public clients we do
312
     * not need to authenticate them
313
     *
314
     * @param ServerRequestInterface $request
315
     * @param bool $allowPublicClients
316
     * @return Client|null
317
     */
318
    protected function getClient(ServerRequestInterface $request, bool $allowPublicClients): ?Client
319
    {
320
        [$id, $secret] = $this->getClientCredentialsFromRequest($request);
321
322
        // If the grant type we are issuing does not allow public clients, and that the secret is
323
        // missing, then we have an error...
324
        if ($allowPublicClients === false && empty($secret)) {
325
            throw OAuth2Exception::invalidClient('Client secret is missing');
326
        }
327
328
        // If we allow public clients and no client id was set, we can return null
329
        if ($allowPublicClients && empty($id)) {
330
            return null;
331
        }
332
333
        $client = $this->clientService->find($id);
334
        // We delegate all the checks to the client service
335
        if ($client === null || ($allowPublicClients === false && $client->authenticate($secret) === false)) {
336
            throw OAuth2Exception::invalidClient('Client authentication failed');
337
        }
338
339
        return $client;
340
    }
341
342
    /**
343
     * Create a response from the exception, using the format of the spec
344
     * @link   http://tools.ietf.org/html/rfc6749#section-5.2
345
     *
346
     * @param OAuth2Exception $exception
347
     * @return ResponseInterface
348
     */
349
    protected function createResponseFromException(OAuth2Exception $exception): ResponseInterface
350
    {
351
        $data = [
352
            'error' => $exception->getCode(),
353
            'error_description' => $exception->getMessage(),
354
        ];
355
356
        $this->logger->error('OAuth2 error: {type}::{code}:{description}', [
357
            'code' => $exception->getCode(),
358
            'description' => $exception->getMessage(),
359
            'type' => get_class($exception),
360
        ]);
361
362
        return new JsonResponse($data, 400);
363
    }
364
365
    /**
366
     * Return the client id and secret from request data
367
     * @param ServerRequestInterface $request
368
     * @return array<string>
369
     */
370
    protected function getClientCredentialsFromRequest(ServerRequestInterface $request): array
371
    {
372
        // We first try to get the Authorization header, as this is
373
        // the recommended way according to the spec
374
        if ($request->hasHeader('Authorization')) {
375
            // Header value is expected to be "Bearer xxx"
376
            $parts = explode(' ', $request->getHeaderLine('Authorization'));
377
            $value = base64_decode(end($parts));
378
379
            [$id, $secret] = explode(':', $value);
380
        } else {
381
            $postParams = (array) $request->getParsedBody();
382
            $id = $postParams['client_id'] ?? null;
383
            $secret = $postParams['client_secret'] ?? null;
384
        }
385
386
        return [$id, $secret];
387
    }
388
}
389