Passed
Pull Request — master (#1298)
by
unknown
62:34 queued 27:34
created

OAuthServerException::invalidRefreshToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
/**
4
 * @author      Alex Bilbie <[email protected]>
5
 * @copyright   Copyright (c) Alex Bilbie
6
 * @license     http://mit-license.org/
7
 *
8
 * @link        https://github.com/thephpleague/oauth2-server
9
 */
10
11
declare(strict_types=1);
12
13
namespace League\OAuth2\Server\Exception;
14
15
use Exception;
16
use Psr\Http\Message\ResponseInterface;
17
use Psr\Http\Message\ServerRequestInterface;
18
use Throwable;
19
20
use function htmlspecialchars;
21
use function http_build_query;
22
use function sprintf;
23
use function strpos;
24
use function strstr;
25
26
class OAuthServerException extends Exception
27
{
28
    /**
29
     * @var array<string, string>
30
     */
31
    private array $payload;
32
33
    private ServerRequestInterface $serverRequest;
34
35
    /**
36
     * Throw a new exception.
37
     */
38 91
    final public function __construct(string $message, int $code, private string $errorType, private int $httpStatusCode = 400, private ?string $hint = null, private ?string $redirectUri = null, Throwable $previous = null, private ?string $queryDelimiter = '?')
39
    {
40 91
        parent::__construct($message, $code, $previous);
41 91
        $this->payload = [
42 91
            'error'             => $errorType,
43 91
            'error_description' => $message,
44 91
        ];
45
46 91
        if ($hint !== null) {
47 58
            $this->payload['hint'] = $hint;
48
        }
49
    }
50
51
    /**
52
     * Returns the current payload.
53
     *
54
     * @return array<string, string>
55
     */
56 10
    public function getPayload(): array
57
    {
58 10
        return $this->payload;
59
    }
60
61
    /**
62
     * Updates the current payload.
63
     *
64
     * @param array<string, string> $payload
65
     */
66
    public function setPayload(array $payload): void
67
    {
68
        $this->payload = $payload;
69
    }
70
71
    /**
72
     * Set the server request that is responsible for generating the exception
73
     */
74 19
    public function setServerRequest(ServerRequestInterface $serverRequest): void
75
    {
76 19
        $this->serverRequest = $serverRequest;
77
    }
78
79
    /**
80
     * Unsupported grant type error.
81
     */
82 2
    public static function unsupportedGrantType(): static
83
    {
84 2
        $errorMessage = 'The authorization grant type is not supported by the authorization server.';
85 2
        $hint = 'Check that all required parameters have been provided';
86
87 2
        return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint);
88
    }
89
90
    /**
91
     * Invalid request error.
92
     */
93 25
    public static function invalidRequest(string $parameter, ?string $hint = null, Throwable $previous = null): static
94
    {
95 25
        $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' .
96 25
            'includes a parameter more than once, or is otherwise malformed.';
97 25
        $hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint;
98
99 25
        return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous);
100
    }
101
102
    /**
103
     * Invalid client error.
104
     */
105 19
    public static function invalidClient(ServerRequestInterface $serverRequest): static
106
    {
107 19
        $exception = new static('Client authentication failed', 4, 'invalid_client', 401);
108
109 19
        $exception->setServerRequest($serverRequest);
110
111 19
        return $exception;
112
    }
113
114
    /**
115
     * Invalid scope error
116
     */
117 8
    public static function invalidScope(string $scope, string|null $redirectUri = null, string $queryDelimiter = '?'): static
118
    {
119 8
        $errorMessage = 'The requested scope is invalid, unknown, or malformed';
120
121 8
        if ($scope === '') {
122
            $hint = 'Specify a scope in the request or set a default scope';
123
        } else {
124 8
            $hint = sprintf(
125 8
                'Check the `%s` scope',
126 8
                htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false)
127 8
            );
128
        }
129
130 8
        return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri, null, $queryDelimiter);
131
    }
132
133
    /**
134
     * Invalid credentials error.
135
     */
136 2
    public static function invalidCredentials(): static
137
    {
138 2
        return new static('The user credentials were incorrect.', 6, 'invalid_grant', 400);
139
    }
140
141
    /**
142
     * Server error.
143
     *
144
     * @codeCoverageIgnore
145
     */
146
    public static function serverError(string $hint, Throwable $previous = null): static
147
    {
148
        return new static(
149
            'The authorization server encountered an unexpected condition which prevented it from fulfilling'
150
            . ' the request: ' . $hint,
151
            7,
152
            'server_error',
153
            500,
154
            null,
155
            null,
156
            $previous
157
        );
158
    }
159
160
    /**
161
     * Invalid refresh token.
162
     */
163 4
    public static function invalidRefreshToken(?string $hint = null, Throwable $previous = null): static
164
    {
165 4
        return new static('The refresh token is invalid.', 8, 'invalid_grant', 400, $hint, null, $previous);
166
    }
167
168
    /**
169
     * Access denied.
170
     */
171 17
    public static function accessDenied(?string $hint = null, ?string $redirectUri = null, Throwable $previous = null, string $queryDelimiter = '?'): static
172
    {
173 17
        return new static(
174 17
            'The resource owner or authorization server denied the request.',
175 17
            9,
176 17
            'access_denied',
177 17
            401,
178 17
            $hint,
179 17
            $redirectUri,
180 17
            $previous,
181 17
            $queryDelimiter
182
        );
183
    }
184
185
    /**
186
     * Invalid grant.
187 2
     */
188
    public static function invalidGrant(string $hint = ''): static
189 2
    {
190 2
        return new static(
191 2
            'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token '
192 2
                . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, '
193 2
                . 'or was issued to another client.',
194 2
            10,
195 2
            'invalid_grant',
196 2
            400,
197 2
            $hint
198
        );
199
    }
200 3
201
    public function getErrorType(): string
202 3
    {
203
        return $this->errorType;
204
    }
205
206
    /**
207
     * Expired token error.
208
     *
209
     * @param Throwable $previous Previous exception
210
     *
211
     * @return static
212 1
     */
213
    public static function expiredToken(?string $hint = null, Throwable $previous = null): static
214 1
    {
215 1
        $errorMessage = 'The `device_code` has expired and the device ' .
216
                        'authorization session has concluded.';
217 1
218
        return new static($errorMessage, 11, 'expired_token', 400, $hint, null, $previous);
219
    }
220 1
221
    public static function authorizationPending(string $hint = '', Throwable $previous = null): static
222 1
    {
223 1
        return new static(
224 1
            'The authorization request is still pending as the end user ' .
225 1
            'hasn\'t yet completed the user interaction steps. The client ' .
226 1
            'SHOULD repeat the Access Token Request to the token endpoint',
227 1
            12,
228 1
            'authorization_pending',
229 1
            400,
230 1
            $hint,
231 1
            null,
232 1
            $previous
233
        );
234
    }
235
236
    /**
237
     * Slow down error used with the Device Authorization Grant.
238
     *
239
     *
240
     * @return static
241 1
     */
242
    public static function slowDown(string $hint = '', Throwable $previous = null): static
243 1
    {
244 1
        return new static(
245 1
            'The authorization request is still pending and polling should ' .
246 1
                'continue, but the interval MUST be increased ' .
247 1
                'by 5 seconds for this and all subsequent requests.',
248 1
            13,
249 1
            'slow_down',
250 1
            400,
251 1
            $hint,
252 1
            null,
253 1
            $previous
254
        );
255
    }
256
257
    /**
258
    {
259
        return $this->errorType;
260
    }
261
262
    /**
263
     * Generate a HTTP response.
264 10
     */
265
    public function generateHttpResponse(ResponseInterface $response, bool $useFragment = false, int $jsonOptions = 0): ResponseInterface
266 10
    {
267
        $headers = $this->getHttpHeaders();
268 10
269
        $payload = $this->getPayload();
270 10
271 3
        if ($this->redirectUri !== null) {
272 1
            $queryDelimiter = $useFragment === true ? '#' : $this->queryDelimiter;
273
            $this->redirectUri .= (strstr($this->redirectUri, $queryDelimiter) === false) ? $queryDelimiter : '&';
0 ignored issues
show
Bug introduced by
It seems like $queryDelimiter can also be of type null; however, parameter $needle of strstr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

273
            $this->redirectUri .= (strstr($this->redirectUri, /** @scrutinizer ignore-type */ $queryDelimiter) === false) ? $queryDelimiter : '&';
Loading history...
274 2
275
            return $response->withStatus(302)->withHeader('Location', $this->redirectUri . http_build_query($payload));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withSt..._build_query($payload)) returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface.
Loading history...
276
        }
277 3
278
        foreach ($headers as $header => $content) {
279
            $response = $response->withHeader($header, $content);
280 7
        }
281 7
282
        $jsonEncodedPayload = json_encode($payload, $jsonOptions);
283
284 7
        $responseBody = $jsonEncodedPayload === false ? 'JSON encoding of payload failed' : $jsonEncodedPayload;
285
286 7
        $response->getBody()->write($responseBody);
287
288 7
        return $response->withStatus($this->getHttpStatusCode());
0 ignored issues
show
Bug introduced by
The method withStatus() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Psr\Http\Message\ResponseInterface. ( Ignorable by Annotation )

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

288
        return $response->/** @scrutinizer ignore-call */ withStatus($this->getHttpStatusCode());
Loading history...
289
    }
290 7
291
    /**
292
     * Get all headers that have to be send with the error response.
293
     *
294
     * @return array<string, string> Array with header values
295
     */
296
    public function getHttpHeaders(): array
297
    {
298 10
        $headers = [
299
            'Content-type' => 'application/json',
300 10
        ];
301 10
302 10
        // Add "WWW-Authenticate" header
303
        //
304
        // RFC 6749, section 5.2.:
305
        // "If the client attempted to authenticate via the 'Authorization'
306
        // request header field, the authorization server MUST
307
        // respond with an HTTP 401 (Unauthorized) status code and
308
        // include the "WWW-Authenticate" response header field
309
        // matching the authentication scheme used by the client.
310
        if ($this->errorType === 'invalid_client' && $this->requestHasAuthorizationHeader()) {
311
            $authScheme = strpos($this->serverRequest->getHeader('Authorization')[0], 'Bearer') === 0 ? 'Bearer' : 'Basic';
312 10
313 2
            $headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"';
314
        }
315 2
316
        return $headers;
317
    }
318 10
319
    /**
320
     * Check if the exception has an associated redirect URI.
321
     *
322
     * Returns whether the exception includes a redirect, since
323
     * getHttpStatusCode() doesn't return a 302 when there's a
324
     * redirect enabled. This helps when you want to override local
325
     * error pages but want to let redirects through.
326
     */
327
    public function hasRedirect(): bool
328
    {
329 2
        return $this->redirectUri !== null;
330
    }
331 2
332
    /**
333
     * Returns the Redirect URI used for redirecting.
334
     */
335
    public function getRedirectUri(): ?string
336
    {
337 1
        return $this->redirectUri;
338
    }
339 1
340
    /**
341
     * Returns the HTTP status code to send when the exceptions is output.
342
     */
343
    public function getHttpStatusCode(): int
344
    {
345 8
        return $this->httpStatusCode;
346
    }
347 8
348
    public function getHint(): ?string
349
    {
350 15
        return $this->hint;
351
    }
352 15
353
    /**
354
     * Check if the request has a non-empty 'Authorization' header value.
355
     *
356
     * Returns true if the header is present and not an empty string, false
357
     * otherwise.
358
     */
359
    private function requestHasAuthorizationHeader(): bool
360
    {
361 5
        if (!$this->serverRequest->hasHeader('Authorization')) {
362
            return false;
363 5
        }
364 2
365
        $authorizationHeader = $this->serverRequest->getHeader('Authorization');
366
367 3
        // Common .htaccess configurations yield an empty string for the
368
        // 'Authorization' header when one is not provided by the client.
369
        // For practical purposes that case should be treated as though the
370
        // header isn't present.
371
        // See https://github.com/thephpleague/oauth2-server/issues/1162
372
        if ($authorizationHeader === [] || $authorizationHeader[0] === '') {
373
            return false;
374 3
        }
375 1
376
        return true;
377
    }
378
}
379