Passed
Pull Request — master (#1298)
by
unknown
35:09
created

OAuthServerException::getRedirectUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 1
cts 1
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);
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

87
        return /** @scrutinizer ignore-call */ new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

99
        return /** @scrutinizer ignore-call */ new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

107
        $exception = /** @scrutinizer ignore-call */ new static('Client authentication failed', 4, 'invalid_client', 401);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

138
        return /** @scrutinizer ignore-call */ new static('The user credentials were incorrect.', 6, 'invalid_grant', 400);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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(
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

148
        return /** @scrutinizer ignore-call */ new static(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

165
        return /** @scrutinizer ignore-call */ new static('The refresh token is invalid.', 8, 'invalid_grant', 400, $hint, null, $previous);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
166
    }
167
168
    /**
169
     * Access denied.
170
     */
171 17
    public static function accessDenied(
172
        ?string $hint = null,
173 17
        ?string $redirectUri = null,
174 17
        Throwable $previous = null,
175 17
        string $queryDelimiter = '?'
176 17
    ): static {
177 17
        return new static(
178 17
            'The resource owner or authorization server denied the request.',
179 17
            9,
180 17
            'access_denied',
181 17
            401,
182
            $hint,
183
            $redirectUri,
184
            $previous,
185
            $queryDelimiter
186
        );
187 2
    }
188
189 2
    /**
190 2
     * Invalid grant.
191 2
     */
192 2
    public static function invalidGrant(string $hint = ''): static
193 2
    {
194 2
        return new static(
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

194
        return /** @scrutinizer ignore-call */ new static(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
195 2
            'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token '
196 2
                . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, '
197 2
                . 'or was issued to another client.',
198
            10,
199
            'invalid_grant',
200 3
            400,
201
            $hint
202 3
        );
203
    }
204
205
    public function getErrorType(): string
206
    {
207
        return $this->errorType;
208
    }
209
210
    /**
211
     * Expired token error.
212 1
     *
213
     * @param Throwable $previous Previous exception
214 1
     *
215 1
     * @return static
216
     */
217 1
    public static function expiredToken(?string $hint = null, Throwable $previous = null): static
218
    {
219
        $errorMessage = 'The `device_code` has expired and the device ' .
220 1
                        'authorization session has concluded.';
221
222 1
        return new static($errorMessage, 11, 'expired_token', 400, $hint, null, $previous);
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

222
        return /** @scrutinizer ignore-call */ new static($errorMessage, 11, 'expired_token', 400, $hint, null, $previous);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
223 1
    }
224 1
225 1
    public static function authorizationPending(string $hint = '', Throwable $previous = null): static
226 1
    {
227 1
        return new static(
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

227
        return /** @scrutinizer ignore-call */ new static(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
228 1
            'The authorization request is still pending as the end user ' .
229 1
            'hasn\'t yet completed the user interaction steps. The client ' .
230 1
            'SHOULD repeat the Access Token Request to the token endpoint',
231 1
            12,
232 1
            'authorization_pending',
233
            400,
234
            $hint,
235
            null,
236
            $previous
237
        );
238
    }
239
240
    /**
241 1
     * Slow down error used with the Device Authorization Grant.
242
     *
243 1
     *
244 1
     * @return static
245 1
     */
246 1
    public static function slowDown(string $hint = '', Throwable $previous = null): static
247 1
    {
248 1
        return new static(
0 ignored issues
show
Bug introduced by
The call to League\OAuth2\Server\Exc...xception::__construct() has too few arguments starting with queryDelimiter. ( Ignorable by Annotation )

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

248
        return /** @scrutinizer ignore-call */ new static(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
249 1
            'The authorization request is still pending and polling should ' .
250 1
                'continue, but the interval MUST be increased ' .
251 1
                'by 5 seconds for this and all subsequent requests.',
252 1
            13,
253 1
            'slow_down',
254
            400,
255
            $hint,
256
            null,
257
            $previous
258
        );
259
    }
260
261
    /**
262
    {
263
        return $this->errorType;
264 10
    }
265
266 10
    /**
267
     * Generate a HTTP response.
268 10
     */
269
    public function generateHttpResponse(ResponseInterface $response, bool $useFragment = false, int $jsonOptions = 0): ResponseInterface
270 10
    {
271 3
        $headers = $this->getHttpHeaders();
272 1
273
        $payload = $this->getPayload();
274 2
275
        if ($this->redirectUri !== null) {
276
            $queryDelimiter = $useFragment === true ? '#' : $this->queryDelimiter;
277 3
            $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

277
            $this->redirectUri .= (strstr($this->redirectUri, /** @scrutinizer ignore-type */ $queryDelimiter) === false) ? $queryDelimiter : '&';
Loading history...
278
279
            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...
280 7
        }
281 7
282
        foreach ($headers as $header => $content) {
283
            $response = $response->withHeader($header, $content);
284 7
        }
285
286 7
        $jsonEncodedPayload = json_encode($payload, $jsonOptions);
287
288 7
        $responseBody = $jsonEncodedPayload === false ? 'JSON encoding of payload failed' : $jsonEncodedPayload;
289
290 7
        $response->getBody()->write($responseBody);
291
292
        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

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