Passed
Pull Request — master (#1473)
by
unknown
34:47
created

AbstractHandler::setRefreshTokenRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace League\OAuth2\Server;
6
7
use Exception;
8
use League\OAuth2\Server\Entities\ClientEntityInterface;
9
use League\OAuth2\Server\EventEmitting\EmitterAwareInterface;
10
use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill;
11
use League\OAuth2\Server\Exception\OAuthServerException;
12
use League\OAuth2\Server\Grant\GrantTypeInterface;
13
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
14
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
15
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
16
use Psr\Http\Message\ServerRequestInterface;
17
18
use function base64_decode;
19
use function explode;
20
use function json_decode;
21
use function substr;
22
use function time;
23
use function trim;
24
25
abstract class AbstractHandler implements EmitterAwareInterface
26
{
27
    use EmitterAwarePolyfill;
28
    use CryptTrait;
29
30
    protected ClientRepositoryInterface $clientRepository;
31
32
    protected AccessTokenRepositoryInterface $accessTokenRepository;
33
34
    protected RefreshTokenRepositoryInterface $refreshTokenRepository;
35
36
    public function setClientRepository(ClientRepositoryInterface $clientRepository): void
37
    {
38
        $this->clientRepository = $clientRepository;
39
    }
40
41
    public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void
42
    {
43
        $this->accessTokenRepository = $accessTokenRepository;
44
    }
45
46
    public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void
47
    {
48
        $this->refreshTokenRepository = $refreshTokenRepository;
49
    }
50
51
    /**
52
     * Validate the client.
53
     *
54
     * @throws OAuthServerException
55
     */
56
    protected function validateClient(ServerRequestInterface $request): ClientEntityInterface
57
    {
58
        [$clientId, $clientSecret] = $this->getClientCredentials($request);
59
60
        $client = $this->getClientEntityOrFail($clientId, $request);
61
62
        if ($client->isConfidential()) {
63
            if ($clientSecret === '') {
64
                throw OAuthServerException::invalidRequest('client_secret');
65
            }
66
67
            if (
68
                $this->clientRepository->validateClient(
69
                    $clientId,
70
                    $clientSecret,
71
                    $this instanceof GrantTypeInterface ? $this->getIdentifier() : null
72
                ) === false
73
            ) {
74
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
75
76
                throw OAuthServerException::invalidClient($request);
77
            }
78
        }
79
80
        return $client;
81
    }
82
83
    /**
84
     * Wrapper around ClientRepository::getClientEntity() that ensures we emit
85
     * an event and throw an exception if the repo doesn't return a client
86
     * entity.
87
     *
88
     * This is a bit of defensive coding because the interface contract
89
     * doesn't actually enforce non-null returns/exception-on-no-client so
90
     * getClientEntity might return null. By contrast, this method will
91
     * always either return a ClientEntityInterface or throw.
92
     *
93
     * @throws OAuthServerException
94
     */
95
    protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
96
    {
97
        $client = $this->clientRepository->getClientEntity($clientId);
98
99
        if ($client instanceof ClientEntityInterface === false) {
100
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
101
            throw OAuthServerException::invalidClient($request);
102
        }
103
104
        return $client;
105
    }
106
107
    /**
108
     * Gets the client credentials from the request from the request body or
109
     * the Http Basic Authorization header
110
     *
111
     * @return array{0:non-empty-string,1:string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{0:non-empty-string,1:string} at position 4 could not be parsed: Unknown type name 'non-empty-string' at position 4 in array{0:non-empty-string,1:string}.
Loading history...
112
     *
113
     * @throws OAuthServerException
114
     */
115
    protected function getClientCredentials(ServerRequestInterface $request): array
116
    {
117
        [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request);
118
119
        $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
120
121
        if ($clientId === null) {
122
            throw OAuthServerException::invalidRequest('client_id');
123
        }
124
125
        $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
126
127
        return [$clientId, $clientSecret ?? ''];
128
    }
129
130
    /**
131
     * Parse request parameter.
132
     *
133
     * @param array<array-key, mixed> $request
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, mixed> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, mixed>.
Loading history...
134
     *
135
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
136
     *
137
     * @throws OAuthServerException
138
     */
139
    private static function parseParam(string $parameter, array $request, ?string $default = null): ?string
140
    {
141
        $value = $request[$parameter] ?? '';
142
143
        if (is_scalar($value)) {
144
            $value = trim((string) $value);
145
        } else {
146
            throw OAuthServerException::invalidRequest($parameter);
147
        }
148
149
        if ($value === '') {
150
            $value = $default === null ? null : trim($default);
151
152
            if ($value === '') {
153
                $value = null;
154
            }
155
        }
156
157
        return $value;
158
    }
159
160
    /**
161
     * Retrieve request parameter.
162
     *
163
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
164
     *
165
     * @throws OAuthServerException
166
     */
167
    protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
168
    {
169
        return self::parseParam($parameter, (array) $request->getParsedBody(), $default);
170
    }
171
172
    /**
173
     * Retrieve HTTP Basic Auth credentials with the Authorization header
174
     * of a request. First index of the returned array is the username,
175
     * second is the password (so list() will work). If the header does
176
     * not exist, or is otherwise an invalid HTTP Basic header, return
177
     * [null, null].
178
     *
179
     * @return array{0:non-empty-string,1:string}|array{0:null,1:null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{0:non-empty-string...g}|array{0:null,1:null} at position 4 could not be parsed: Unknown type name 'non-empty-string' at position 4 in array{0:non-empty-string,1:string}|array{0:null,1:null}.
Loading history...
180
     */
181
    protected function getBasicAuthCredentials(ServerRequestInterface $request): array
182
    {
183
        if (!$request->hasHeader('Authorization')) {
184
            return [null, null];
185
        }
186
187
        $header = $request->getHeader('Authorization')[0];
188
        if (stripos($header, 'Basic ') !== 0) {
189
            return [null, null];
190
        }
191
192
        $decoded = base64_decode(substr($header, 6), true);
193
194
        if ($decoded === false) {
195
            return [null, null];
196
        }
197
198
        if (str_contains($decoded, ':') === false) {
199
            return [null, null]; // HTTP Basic header without colon isn't valid
200
        }
201
202
        [$username, $password] = explode(':', $decoded, 2);
203
204
        if ($username === '') {
205
            return [null, null];
206
        }
207
208
        return [$username, $password];
209
    }
210
211
    /**
212
     * Retrieve query string parameter.
213
     *
214
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
215
     *
216
     * @throws OAuthServerException
217
     */
218
    protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
219
    {
220
        return self::parseParam($parameter, $request->getQueryParams(), $default);
221
    }
222
223
    /**
224
     * Retrieve cookie parameter.
225
     *
226
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
227
     *
228
     * @throws OAuthServerException
229
     */
230
    protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
231
    {
232
        return self::parseParam($parameter, $request->getCookieParams(), $default);
233
    }
234
235
    /**
236
     * Retrieve server parameter.
237
     *
238
     * @return non-empty-string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string|null at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string|null.
Loading history...
239
     *
240
     * @throws OAuthServerException
241
     */
242
    protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string
243
    {
244
        return self::parseParam($parameter, $request->getServerParams(), $default);
245
    }
246
247
    /**
248
     * Validate the given encrypted refresh token.
249
     *
250
     * @throws OAuthServerException
251
     *
252
     * @return array<non-empty-string, mixed>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string, mixed> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string, mixed>.
Loading history...
253
     */
254
    protected function validateEncryptedRefreshToken(
255
        ServerRequestInterface $request,
256
        string $encryptedRefreshToken,
257
        string $clientId
258
    ): array {
259
        try {
260
            $refreshToken = $this->decrypt($encryptedRefreshToken);
261
        } catch (Exception $e) {
262
            throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
263
        }
264
265
        $refreshTokenData = json_decode($refreshToken, true);
266
267
        if ($refreshTokenData['client_id'] !== $clientId) {
268
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
269
            throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
270
        }
271
272
        if ($refreshTokenData['expire_time'] < time()) {
273
            throw OAuthServerException::invalidRefreshToken('Token has expired');
274
        }
275
276
        if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {
277
            throw OAuthServerException::invalidRefreshToken('Token has been revoked');
278
        }
279
280
        return $refreshTokenData;
281
    }
282
}
283