Passed
Pull Request — master (#1298)
by
unknown
33:38
created

OAuthServerException   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Test Coverage

Coverage 97.62%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 107
c 3
b 1
f 0
dl 0
loc 351
ccs 123
cts 126
cp 0.9762
rs 9.36
wmc 38

24 Methods

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

271
            $this->redirectUri .= (str_contains($this->redirectUri, /** @scrutinizer ignore-type */ $queryDelimiter) === false) ? $queryDelimiter : '&';
Loading history...
272 2
273
            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...
274
        }
275 3
276
        foreach ($headers as $header => $content) {
277
            $response = $response->withHeader($header, $content);
278 7
        }
279 7
280
        $jsonEncodedPayload = json_encode($payload, $jsonOptions);
281
282 7
        $responseBody = $jsonEncodedPayload === false ? 'JSON encoding of payload failed' : $jsonEncodedPayload;
283
284 7
        $response->getBody()->write($responseBody);
285
286 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

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