OAuthServerException::invalidClient()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 93
    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)
37
    {
38 93
        parent::__construct($message, $code, $previous);
39 93
        $this->payload = [
40 93
            'error'             => $errorType,
41 93
            'error_description' => $message,
42 93
        ];
43
44 93
        if ($hint !== null) {
45 63
            $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 15
    public function setServerRequest(ServerRequestInterface $serverRequest): void
73
    {
74 15
        $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 26
    public static function invalidRequest(string $parameter, ?string $hint = null, ?Throwable $previous = null): static
92
    {
93 26
        $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' .
94 26
            'includes a parameter more than once, or is otherwise malformed.';
95 26
        $hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint;
96
97 26
        return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous);
98
    }
99
100
    /**
101
     * Invalid client error.
102
     */
103 15
    public static function invalidClient(ServerRequestInterface $serverRequest): static
104
    {
105 15
        $exception = new static('Client authentication failed', 4, 'invalid_client', 401);
106
107 15
        $exception->setServerRequest($serverRequest);
108
109 15
        return $exception;
110
    }
111
112
    /**
113
     * Invalid scope error
114
     */
115 10
    public static function invalidScope(string $scope, string|null $redirectUri = null): static
116
    {
117 10
        $errorMessage = 'The requested scope is invalid, unknown, or malformed';
118
119 10
        if ($scope === '') {
120
            $hint = 'Specify a scope in the request or set a default scope';
121
        } else {
122 10
            $hint = sprintf(
123 10
                'Check the `%s` scope',
124 10
                htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false)
125 10
            );
126
        }
127
128 10
        return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri);
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): 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
        );
180
    }
181
182
    /**
183
     * Invalid grant.
184
     */
185 4
    public static function invalidGrant(string $hint = ''): static
186
    {
187 4
        return new static(
188 4
            'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token '
189 4
                . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, '
190 4
                . 'or was issued to another client.',
191 4
            10,
192 4
            'invalid_grant',
193 4
            400,
194 4
            $hint
195 4
        );
196
    }
197
198 9
    public function getErrorType(): string
199
    {
200 9
        return $this->errorType;
201
    }
202
203
    /**
204
     * Expired token error.
205
     *
206
     * @param Throwable $previous Previous exception
207
     *
208
     * @return static
209
     */
210 1
    public static function expiredToken(?string $hint = null, ?Throwable $previous = null): static
211
    {
212 1
        $errorMessage = 'The `device_code` has expired and the device ' .
213 1
                        'authorization session has concluded.';
214
215 1
        return new static($errorMessage, 11, 'expired_token', 400, $hint, null, $previous);
216
    }
217
218 1
    public static function authorizationPending(string $hint = '', ?Throwable $previous = null): static
219
    {
220 1
        return new static(
221 1
            'The authorization request is still pending as the end user ' .
222 1
            'hasn\'t yet completed the user interaction steps. The client ' .
223 1
            'SHOULD repeat the Access Token Request to the token endpoint',
224 1
            12,
225 1
            'authorization_pending',
226 1
            400,
227 1
            $hint,
228 1
            null,
229 1
            $previous
230 1
        );
231
    }
232
233
    /**
234
     * Slow down error used with the Device Authorization Grant.
235
     *
236
     *
237
     * @return static
238
     */
239 1
    public static function slowDown(string $hint = '', ?Throwable $previous = null): static
240
    {
241 1
        return new static(
242 1
            'The authorization request is still pending and polling should ' .
243 1
                'continue, but the interval MUST be increased ' .
244 1
                'by 5 seconds for this and all subsequent requests.',
245 1
            13,
246 1
            'slow_down',
247 1
            400,
248 1
            $hint,
249 1
            null,
250 1
            $previous
251 1
        );
252
    }
253
254
    /**
255
     * Unauthorized client error.
256
     */
257 1
    public static function unauthorizedClient(?string $hint = null): static
258
    {
259 1
        return new static(
260 1
            'The authenticated client is not authorized to use this authorization grant type.',
261 1
            14,
262 1
            'unauthorized_client',
263 1
            400,
264 1
            $hint
265 1
        );
266
    }
267
268
    /**
269
     * Generate a HTTP response.
270
     */
271 10
    public function generateHttpResponse(ResponseInterface $response, bool $useFragment = false, int $jsonOptions = 0): ResponseInterface
272
    {
273 10
        $headers = $this->getHttpHeaders();
274
275 10
        $payload = $this->getPayload();
276
277 10
        if ($this->redirectUri !== null) {
278 3
            if ($useFragment === true) {
279 1
                $this->redirectUri .= (str_contains($this->redirectUri, '#') === false) ? '#' : '&';
280
            } else {
281 2
                $this->redirectUri .= (str_contains($this->redirectUri, '?') === false) ? '?' : '&';
282
            }
283
284 3
            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...
285
        }
286
287 7
        foreach ($headers as $header => $content) {
288 7
            $response = $response->withHeader($header, $content);
289
        }
290
291 7
        $jsonEncodedPayload = json_encode($payload, $jsonOptions);
292
293 7
        $responseBody = $jsonEncodedPayload === false ? 'JSON encoding of payload failed' : $jsonEncodedPayload;
294
295 7
        $response->getBody()->write($responseBody);
296
297 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

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