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
![]() |
|||||
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
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
![]() |
|||||
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 |