AuthorizationServer::__construct()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 5
nop 5
dl 0
loc 17
rs 10
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\OAuthJsonResponse;
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 grant list
59
     * @var array<string, GrantInterface>
60
     */
61
    protected array $grants = [];
62
63
    /**
64
     * A list of grant that can answer to an authorization request
65
     * @var array<string, GrantInterface>
66
     */
67
    protected array $responseTypes = [];
68
69
    /**
70
     * Create new instance
71
     * @param ClientService $clientService
72
     * @param AccessTokenService $accessTokenService
73
     * @param RefreshTokenService $refreshTokenService
74
     * @param LoggerInterface $logger
75
     * @param GrantInterface[] $grants
76
     */
77
    public function __construct(
78
        protected ClientService $clientService,
79
        protected AccessTokenService $accessTokenService,
80
        protected RefreshTokenService $refreshTokenService,
81
        protected LoggerInterface $logger,
82
        array $grants = []
83
    ) {
84
        foreach ($grants as /** @var GrantInterface $grant */ $grant) {
85
            if ($grant instanceof AuthorizationServerAwareInterface) {
86
                $grant->setAuthorizationServer($this);
87
            }
88
89
            $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

89
            $this->grants[$grant->/** @scrutinizer ignore-call */ getType()] = $grant;
Loading history...
90
91
            $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

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