Passed
Push — develop ( f140eb...658058 )
by nguereza
01:41
created

getClientCredentialsFromRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 17
rs 9.9666
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\OAuth2\AuthorizationServerInterface;
39
use Platine\OAuth2\Entity\Client;
40
use Platine\OAuth2\Entity\TokenOwnerInterface;
41
use Platine\OAuth2\Exception\OAuth2Exception;
42
use Platine\OAuth2\Grant\AuthorizationServerAwareInterface;
43
use Platine\OAuth2\Grant\GrantInterface;
44
use Platine\OAuth2\Response\JsonResponse;
45
use Platine\OAuth2\Service\AccessTokenService;
46
use Platine\OAuth2\Service\ClientService;
47
use Platine\OAuth2\Service\RefreshTokenService;
48
use Throwable;
49
50
/**
51
 * @class AuthorizationServer
52
 * @package Platine\OAuth2
53
 */
54
class AuthorizationServer implements AuthorizationServerInterface
55
{
56
    /**
57
     * The ClientService
58
     * @var ClientService
59
     */
60
    protected ClientService $clientService;
61
62
    /**
63
     * The AccessTokenService
64
     * @var AccessTokenService
65
     */
66
    protected AccessTokenService $accessTokenService;
67
68
    /**
69
     * The RefreshTokenService
70
     * @var RefreshTokenService
71
     */
72
    protected RefreshTokenService $refreshTokenService;
73
74
75
    /**
76
     * The grant list
77
     * @var array<string, GrantInterface>
78
     */
79
    protected array $grants = [];
80
81
    /**
82
     * A list of grant that can answer to an authorization request
83
     * @var array<string, GrantInterface>
84
     */
85
    protected array $responseTypes = [];
86
87
    /**
88
     * Create new instance
89
     * @param ClientService $clientService
90
     * @param AccessTokenService $accessTokenService
91
     * @param RefreshTokenService $refreshTokenService
92
     * @param array<string, GrantInterface> $grants
93
     */
94
    public function __construct(
95
        ClientService $clientService,
96
        AccessTokenService $accessTokenService,
97
        RefreshTokenService $refreshTokenService,
98
        array $grants = []
99
    ) {
100
        $this->clientService = $clientService;
101
        $this->accessTokenService = $accessTokenService;
102
        $this->refreshTokenService = $refreshTokenService;
103
104
        foreach ($grants as /** @var GrantInterface $grant */ $grant) {
105
            if ($grant instanceof AuthorizationServerAwareInterface) {
106
                $grant->setAuthorizationServer($this);
107
            }
108
109
            $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

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

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