thephpleague /
oauth2-server
| 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
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
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
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 |