vendor/league/oauth2-server/src/Grant/AuthCodeGrant.php line 260

Open in your IDE?
  1. <?php
  2. /**
  3.  * @author      Alex Bilbie <hello@alexbilbie.com>
  4.  * @copyright   Copyright (c) Alex Bilbie
  5.  * @license     http://mit-license.org/
  6.  *
  7.  * @link        https://github.com/thephpleague/oauth2-server
  8.  */
  9. namespace League\OAuth2\Server\Grant;
  10. use DateInterval;
  11. use DateTimeImmutable;
  12. use Exception;
  13. use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
  14. use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
  15. use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
  16. use League\OAuth2\Server\Entities\ClientEntityInterface;
  17. use League\OAuth2\Server\Entities\UserEntityInterface;
  18. use League\OAuth2\Server\Exception\OAuthServerException;
  19. use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
  20. use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
  21. use League\OAuth2\Server\RequestAccessTokenEvent;
  22. use League\OAuth2\Server\RequestEvent;
  23. use League\OAuth2\Server\RequestRefreshTokenEvent;
  24. use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
  25. use League\OAuth2\Server\ResponseTypes\RedirectResponse;
  26. use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
  27. use LogicException;
  28. use Psr\Http\Message\ServerRequestInterface;
  29. use stdClass;
  30. class AuthCodeGrant extends AbstractAuthorizeGrant
  31. {
  32.     /**
  33.      * @var DateInterval
  34.      */
  35.     private $authCodeTTL;
  36.     /**
  37.      * @var bool
  38.      */
  39.     private $requireCodeChallengeForPublicClients true;
  40.     /**
  41.      * @var CodeChallengeVerifierInterface[]
  42.      */
  43.     private $codeChallengeVerifiers = [];
  44.     /**
  45.      * @param AuthCodeRepositoryInterface     $authCodeRepository
  46.      * @param RefreshTokenRepositoryInterface $refreshTokenRepository
  47.      * @param DateInterval                    $authCodeTTL
  48.      *
  49.      * @throws Exception
  50.      */
  51.     public function __construct(
  52.         AuthCodeRepositoryInterface $authCodeRepository,
  53.         RefreshTokenRepositoryInterface $refreshTokenRepository,
  54.         DateInterval $authCodeTTL
  55.     ) {
  56.         $this->setAuthCodeRepository($authCodeRepository);
  57.         $this->setRefreshTokenRepository($refreshTokenRepository);
  58.         $this->authCodeTTL $authCodeTTL;
  59.         $this->refreshTokenTTL = new DateInterval('P1M');
  60.         if (\in_array('sha256'\hash_algos(), true)) {
  61.             $s256Verifier = new S256Verifier();
  62.             $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
  63.         }
  64.         $plainVerifier = new PlainVerifier();
  65.         $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
  66.     }
  67.     /**
  68.      * Disable the requirement for a code challenge for public clients.
  69.      */
  70.     public function disableRequireCodeChallengeForPublicClients()
  71.     {
  72.         $this->requireCodeChallengeForPublicClients false;
  73.     }
  74.     /**
  75.      * Respond to an access token request.
  76.      *
  77.      * @param ServerRequestInterface $request
  78.      * @param ResponseTypeInterface  $responseType
  79.      * @param DateInterval           $accessTokenTTL
  80.      *
  81.      * @throws OAuthServerException
  82.      *
  83.      * @return ResponseTypeInterface
  84.      */
  85.     public function respondToAccessTokenRequest(
  86.         ServerRequestInterface $request,
  87.         ResponseTypeInterface $responseType,
  88.         DateInterval $accessTokenTTL
  89.     ) {
  90.         list($clientId) = $this->getClientCredentials($request);
  91.         $client $this->getClientEntityOrFail($clientId$request);
  92.         // Only validate the client if it is confidential
  93.         if ($client->isConfidential()) {
  94.             $this->validateClient($request);
  95.         }
  96.         $encryptedAuthCode $this->getRequestParameter('code'$requestnull);
  97.         if (!\is_string($encryptedAuthCode)) {
  98.             throw OAuthServerException::invalidRequest('code');
  99.         }
  100.         try {
  101.             $authCodePayload \json_decode($this->decrypt($encryptedAuthCode));
  102.             $this->validateAuthorizationCode($authCodePayload$client$request);
  103.             $scopes $this->scopeRepository->finalizeScopes(
  104.                 $this->validateScopes($authCodePayload->scopes),
  105.                 $this->getIdentifier(),
  106.                 $client,
  107.                 $authCodePayload->user_id
  108.             );
  109.         } catch (LogicException $e) {
  110.             throw OAuthServerException::invalidRequest('code''Cannot decrypt the authorization code'$e);
  111.         }
  112.         $codeVerifier $this->getRequestParameter('code_verifier'$requestnull);
  113.         // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
  114.         if (empty($authCodePayload->code_challenge) && $codeVerifier !== null) {
  115.             throw OAuthServerException::invalidRequest(
  116.                 'code_challenge',
  117.                 'code_verifier received when no code_challenge is present'
  118.             );
  119.         }
  120.         if (!empty($authCodePayload->code_challenge)) {
  121.             $this->validateCodeChallenge($authCodePayload$codeVerifier);
  122.         }
  123.         // Issue and persist new access token
  124.         $accessToken $this->issueAccessToken($accessTokenTTL$client$authCodePayload->user_id$scopes);
  125.         $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED$request$accessToken));
  126.         $responseType->setAccessToken($accessToken);
  127.         // Issue and persist new refresh token if given
  128.         $refreshToken $this->issueRefreshToken($accessToken);
  129.         if ($refreshToken !== null) {
  130.             $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED$request$refreshToken));
  131.             $responseType->setRefreshToken($refreshToken);
  132.         }
  133.         // Revoke used auth code
  134.         $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
  135.         return $responseType;
  136.     }
  137.     private function validateCodeChallenge($authCodePayload$codeVerifier)
  138.     {
  139.         if ($codeVerifier === null) {
  140.             throw OAuthServerException::invalidRequest('code_verifier');
  141.         }
  142.         // Validate code_verifier according to RFC-7636
  143.         // @see: https://tools.ietf.org/html/rfc7636#section-4.1
  144.         if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/'$codeVerifier) !== 1) {
  145.             throw OAuthServerException::invalidRequest(
  146.                 'code_verifier',
  147.                 'Code Verifier must follow the specifications of RFC-7636.'
  148.             );
  149.         }
  150.         if (\property_exists($authCodePayload'code_challenge_method')) {
  151.             if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
  152.                 $codeChallengeVerifier $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
  153.                 if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier$authCodePayload->code_challenge) === false) {
  154.                     throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
  155.                 }
  156.             } else {
  157.                 throw OAuthServerException::serverError(
  158.                     \sprintf(
  159.                         'Unsupported code challenge method `%s`',
  160.                         $authCodePayload->code_challenge_method
  161.                     )
  162.                 );
  163.             }
  164.         }
  165.     }
  166.     /**
  167.      * Validate the authorization code.
  168.      *
  169.      * @param stdClass               $authCodePayload
  170.      * @param ClientEntityInterface  $client
  171.      * @param ServerRequestInterface $request
  172.      */
  173.     private function validateAuthorizationCode(
  174.         $authCodePayload,
  175.         ClientEntityInterface $client,
  176.         ServerRequestInterface $request
  177.     ) {
  178.         if (!\property_exists($authCodePayload'auth_code_id')) {
  179.             throw OAuthServerException::invalidRequest('code''Authorization code malformed');
  180.         }
  181.         if (\time() > $authCodePayload->expire_time) {
  182.             throw OAuthServerException::invalidRequest('code''Authorization code has expired');
  183.         }
  184.         if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
  185.             throw OAuthServerException::invalidRequest('code''Authorization code has been revoked');
  186.         }
  187.         if ($authCodePayload->client_id !== $client->getIdentifier()) {
  188.             throw OAuthServerException::invalidRequest('code''Authorization code was not issued to this client');
  189.         }
  190.         // The redirect URI is required in this request
  191.         $redirectUri $this->getRequestParameter('redirect_uri'$requestnull);
  192.         if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
  193.             throw OAuthServerException::invalidRequest('redirect_uri');
  194.         }
  195.         if ($authCodePayload->redirect_uri !== $redirectUri) {
  196.             throw OAuthServerException::invalidRequest('redirect_uri''Invalid redirect URI');
  197.         }
  198.     }
  199.     /**
  200.      * Return the grant identifier that can be used in matching up requests.
  201.      *
  202.      * @return string
  203.      */
  204.     public function getIdentifier()
  205.     {
  206.         return 'authorization_code';
  207.     }
  208.     /**
  209.      * {@inheritdoc}
  210.      */
  211.     public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
  212.     {
  213.         return (
  214.             \array_key_exists('response_type'$request->getQueryParams())
  215.             && $request->getQueryParams()['response_type'] === 'code'
  216.             && isset($request->getQueryParams()['client_id'])
  217.         );
  218.     }
  219.     /**
  220.      * {@inheritdoc}
  221.      */
  222.     public function validateAuthorizationRequest(ServerRequestInterface $request)
  223.     {
  224.         $clientId $this->getQueryStringParameter(
  225.             'client_id',
  226.             $request,
  227.             $this->getServerParameter('PHP_AUTH_USER'$request)
  228.         );
  229.         if ($clientId === null) {
  230.             throw OAuthServerException::invalidRequest('client_id');
  231.         }
  232.         $client $this->getClientEntityOrFail($clientId$request);
  233.         $redirectUri $this->getQueryStringParameter('redirect_uri'$request);
  234.         if ($redirectUri !== null) {
  235.             if (!\is_string($redirectUri)) {
  236.                 throw OAuthServerException::invalidRequest('redirect_uri');
  237.             }
  238.             $this->validateRedirectUri($redirectUri$client$request);
  239.         } elseif (empty($client->getRedirectUri()) ||
  240.             (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
  241.             $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED$request));
  242.             throw OAuthServerException::invalidClient($request);
  243.         }
  244.         $defaultClientRedirectUri \is_array($client->getRedirectUri())
  245.             ? $client->getRedirectUri()[0]
  246.             : $client->getRedirectUri();
  247.         $scopes $this->validateScopes(
  248.             $this->getQueryStringParameter('scope'$request$this->defaultScope),
  249.             $redirectUri ?? $defaultClientRedirectUri
  250.         );
  251.         $stateParameter $this->getQueryStringParameter('state'$request);
  252.         $authorizationRequest = new AuthorizationRequest();
  253.         $authorizationRequest->setGrantTypeId($this->getIdentifier());
  254.         $authorizationRequest->setClient($client);
  255.         $authorizationRequest->setRedirectUri($redirectUri);
  256.         if ($stateParameter !== null) {
  257.             $authorizationRequest->setState($stateParameter);
  258.         }
  259.         $authorizationRequest->setScopes($scopes);
  260.         $codeChallenge $this->getQueryStringParameter('code_challenge'$request);
  261.         if ($codeChallenge !== null) {
  262.             $codeChallengeMethod $this->getQueryStringParameter('code_challenge_method'$request'plain');
  263.             if (\array_key_exists($codeChallengeMethod$this->codeChallengeVerifiers) === false) {
  264.                 throw OAuthServerException::invalidRequest(
  265.                     'code_challenge_method',
  266.                     'Code challenge method must be one of ' \implode(', '\array_map(
  267.                         function ($method) {
  268.                             return '`' $method '`';
  269.                         },
  270.                         \array_keys($this->codeChallengeVerifiers)
  271.                     ))
  272.                 );
  273.             }
  274.             // Validate code_challenge according to RFC-7636
  275.             // @see: https://tools.ietf.org/html/rfc7636#section-4.2
  276.             if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/'$codeChallenge) !== 1) {
  277.                 throw OAuthServerException::invalidRequest(
  278.                     'code_challenge',
  279.                     'Code challenge must follow the specifications of RFC-7636.'
  280.                 );
  281.             }
  282.             $authorizationRequest->setCodeChallenge($codeChallenge);
  283.             $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
  284.         } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
  285.             throw OAuthServerException::invalidRequest('code_challenge''Code challenge must be provided for public clients');
  286.         }
  287.         return $authorizationRequest;
  288.     }
  289.     /**
  290.      * {@inheritdoc}
  291.      */
  292.     public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
  293.     {
  294.         if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
  295.             throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
  296.         }
  297.         $finalRedirectUri $authorizationRequest->getRedirectUri()
  298.                           ?? $this->getClientRedirectUri($authorizationRequest);
  299.         // The user approved the client, redirect them back with an auth code
  300.         if ($authorizationRequest->isAuthorizationApproved() === true) {
  301.             $authCode $this->issueAuthCode(
  302.                 $this->authCodeTTL,
  303.                 $authorizationRequest->getClient(),
  304.                 $authorizationRequest->getUser()->getIdentifier(),
  305.                 $authorizationRequest->getRedirectUri(),
  306.                 $authorizationRequest->getScopes()
  307.             );
  308.             $payload = [
  309.                 'client_id' => $authCode->getClient()->getIdentifier(),
  310.                 'redirect_uri' => $authCode->getRedirectUri(),
  311.                 'auth_code_id' => $authCode->getIdentifier(),
  312.                 'scopes' => $authCode->getScopes(),
  313.                 'user_id' => $authCode->getUserIdentifier(),
  314.                 'expire_time' => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
  315.                 'code_challenge' => $authorizationRequest->getCodeChallenge(),
  316.                 'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
  317.             ];
  318.             $jsonPayload \json_encode($payload);
  319.             if ($jsonPayload === false) {
  320.                 throw new LogicException('An error was encountered when JSON encoding the authorization request response');
  321.             }
  322.             $response = new RedirectResponse();
  323.             $response->setRedirectUri(
  324.                 $this->makeRedirectUri(
  325.                     $finalRedirectUri,
  326.                     [
  327.                         'code' => $this->encrypt($jsonPayload),
  328.                         'state' => $authorizationRequest->getState(),
  329.                     ]
  330.                 )
  331.             );
  332.             return $response;
  333.         }
  334.         // The user denied the client, redirect them back with an error
  335.         throw OAuthServerException::accessDenied(
  336.             'The user denied the request',
  337.             $this->makeRedirectUri(
  338.                 $finalRedirectUri,
  339.                 [
  340.                     'state' => $authorizationRequest->getState(),
  341.                 ]
  342.             )
  343.         );
  344.     }
  345.     /**
  346.      * Get the client redirect URI if not set in the request.
  347.      *
  348.      * @param AuthorizationRequest $authorizationRequest
  349.      *
  350.      * @return string
  351.      */
  352.     private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
  353.     {
  354.         return \is_array($authorizationRequest->getClient()->getRedirectUri())
  355.                 ? $authorizationRequest->getClient()->getRedirectUri()[0]
  356.                 : $authorizationRequest->getClient()->getRedirectUri();
  357.     }
  358. }