vendor/league/oauth2-server/src/Grant/AbstractGrant.php line 202

  1. <?php
  2. /**
  3.  * OAuth 2.0 Abstract grant.
  4.  *
  5.  * @author      Alex Bilbie <hello@alexbilbie.com>
  6.  * @copyright   Copyright (c) Alex Bilbie
  7.  * @license     http://mit-license.org/
  8.  *
  9.  * @link        https://github.com/thephpleague/oauth2-server
  10.  */
  11. namespace League\OAuth2\Server\Grant;
  12. use DateInterval;
  13. use DateTimeImmutable;
  14. use Error;
  15. use Exception;
  16. use League\Event\EmitterAwareTrait;
  17. use League\OAuth2\Server\CryptKey;
  18. use League\OAuth2\Server\CryptTrait;
  19. use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
  20. use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
  21. use League\OAuth2\Server\Entities\ClientEntityInterface;
  22. use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
  23. use League\OAuth2\Server\Entities\ScopeEntityInterface;
  24. use League\OAuth2\Server\Exception\OAuthServerException;
  25. use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
  26. use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
  27. use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
  28. use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
  29. use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
  30. use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
  31. use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
  32. use League\OAuth2\Server\Repositories\UserRepositoryInterface;
  33. use League\OAuth2\Server\RequestEvent;
  34. use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
  35. use LogicException;
  36. use Psr\Http\Message\ServerRequestInterface;
  37. use TypeError;
  38. /**
  39.  * Abstract grant class.
  40.  */
  41. abstract class AbstractGrant implements GrantTypeInterface
  42. {
  43.     use EmitterAwareTraitCryptTrait;
  44.     const SCOPE_DELIMITER_STRING ' ';
  45.     const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS 10;
  46.     /**
  47.      * @var ClientRepositoryInterface
  48.      */
  49.     protected $clientRepository;
  50.     /**
  51.      * @var AccessTokenRepositoryInterface
  52.      */
  53.     protected $accessTokenRepository;
  54.     /**
  55.      * @var ScopeRepositoryInterface
  56.      */
  57.     protected $scopeRepository;
  58.     /**
  59.      * @var AuthCodeRepositoryInterface
  60.      */
  61.     protected $authCodeRepository;
  62.     /**
  63.      * @var RefreshTokenRepositoryInterface
  64.      */
  65.     protected $refreshTokenRepository;
  66.     /**
  67.      * @var UserRepositoryInterface
  68.      */
  69.     protected $userRepository;
  70.     /**
  71.      * @var DateInterval
  72.      */
  73.     protected $refreshTokenTTL;
  74.     /**
  75.      * @var CryptKey
  76.      */
  77.     protected $privateKey;
  78.     /**
  79.      * @var string
  80.      */
  81.     protected $defaultScope;
  82.     /**
  83.      * @var bool
  84.      */
  85.     protected $revokeRefreshTokens;
  86.     /**
  87.      * @param ClientRepositoryInterface $clientRepository
  88.      */
  89.     public function setClientRepository(ClientRepositoryInterface $clientRepository)
  90.     {
  91.         $this->clientRepository $clientRepository;
  92.     }
  93.     /**
  94.      * @param AccessTokenRepositoryInterface $accessTokenRepository
  95.      */
  96.     public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository)
  97.     {
  98.         $this->accessTokenRepository $accessTokenRepository;
  99.     }
  100.     /**
  101.      * @param ScopeRepositoryInterface $scopeRepository
  102.      */
  103.     public function setScopeRepository(ScopeRepositoryInterface $scopeRepository)
  104.     {
  105.         $this->scopeRepository $scopeRepository;
  106.     }
  107.     /**
  108.      * @param RefreshTokenRepositoryInterface $refreshTokenRepository
  109.      */
  110.     public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
  111.     {
  112.         $this->refreshTokenRepository $refreshTokenRepository;
  113.     }
  114.     /**
  115.      * @param AuthCodeRepositoryInterface $authCodeRepository
  116.      */
  117.     public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository)
  118.     {
  119.         $this->authCodeRepository $authCodeRepository;
  120.     }
  121.     /**
  122.      * @param UserRepositoryInterface $userRepository
  123.      */
  124.     public function setUserRepository(UserRepositoryInterface $userRepository)
  125.     {
  126.         $this->userRepository $userRepository;
  127.     }
  128.     /**
  129.      * {@inheritdoc}
  130.      */
  131.     public function setRefreshTokenTTL(DateInterval $refreshTokenTTL)
  132.     {
  133.         $this->refreshTokenTTL $refreshTokenTTL;
  134.     }
  135.     /**
  136.      * Set the private key
  137.      *
  138.      * @param CryptKey $key
  139.      */
  140.     public function setPrivateKey(CryptKey $key)
  141.     {
  142.         $this->privateKey $key;
  143.     }
  144.     /**
  145.      * @param string $scope
  146.      */
  147.     public function setDefaultScope($scope)
  148.     {
  149.         $this->defaultScope $scope;
  150.     }
  151.     /**
  152.      * @param bool $revokeRefreshTokens
  153.      */
  154.     public function revokeRefreshTokens(bool $revokeRefreshTokens)
  155.     {
  156.         $this->revokeRefreshTokens $revokeRefreshTokens;
  157.     }
  158.     /**
  159.      * Validate the client.
  160.      *
  161.      * @param ServerRequestInterface $request
  162.      *
  163.      * @throws OAuthServerException
  164.      *
  165.      * @return ClientEntityInterface
  166.      */
  167.     protected function validateClient(ServerRequestInterface $request)
  168.     {
  169.         [$clientId$clientSecret] = $this->getClientCredentials($request);
  170.         if ($this->clientRepository->validateClient($clientId$clientSecret$this->getIdentifier()) === false) {
  171.             $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED$request));
  172.             throw OAuthServerException::invalidClient($request);
  173.         }
  174.         $client $this->getClientEntityOrFail($clientId$request);
  175.         // If a redirect URI is provided ensure it matches what is pre-registered
  176.         $redirectUri $this->getRequestParameter('redirect_uri'$requestnull);
  177.         if ($redirectUri !== null) {
  178.             if (!\is_string($redirectUri)) {
  179.                 throw OAuthServerException::invalidRequest('redirect_uri');
  180.             }
  181.             $this->validateRedirectUri($redirectUri$client$request);
  182.         }
  183.         return $client;
  184.     }
  185.     /**
  186.      * Wrapper around ClientRepository::getClientEntity() that ensures we emit
  187.      * an event and throw an exception if the repo doesn't return a client
  188.      * entity.
  189.      *
  190.      * This is a bit of defensive coding because the interface contract
  191.      * doesn't actually enforce non-null returns/exception-on-no-client so
  192.      * getClientEntity might return null. By contrast, this method will
  193.      * always either return a ClientEntityInterface or throw.
  194.      *
  195.      * @param string                 $clientId
  196.      * @param ServerRequestInterface $request
  197.      *
  198.      * @return ClientEntityInterface
  199.      */
  200.     protected function getClientEntityOrFail($clientIdServerRequestInterface $request)
  201.     {
  202.         $client $this->clientRepository->getClientEntity($clientId);
  203.         if ($client instanceof ClientEntityInterface === false) {
  204.             $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED$request));
  205.             throw OAuthServerException::invalidClient($request);
  206.         }
  207.         return $client;
  208.     }
  209.     /**
  210.      * Gets the client credentials from the request from the request body or
  211.      * the Http Basic Authorization header
  212.      *
  213.      * @param ServerRequestInterface $request
  214.      *
  215.      * @return array
  216.      */
  217.     protected function getClientCredentials(ServerRequestInterface $request)
  218.     {
  219.         [$basicAuthUser$basicAuthPassword] = $this->getBasicAuthCredentials($request);
  220.         $clientId $this->getRequestParameter('client_id'$request$basicAuthUser);
  221.         if (\is_null($clientId)) {
  222.             throw OAuthServerException::invalidRequest('client_id');
  223.         }
  224.         $clientSecret $this->getRequestParameter('client_secret'$request$basicAuthPassword);
  225.         if ($clientSecret !== null && !\is_string($clientSecret)) {
  226.             throw OAuthServerException::invalidRequest('client_secret');
  227.         }
  228.         return [$clientId$clientSecret];
  229.     }
  230.     /**
  231.      * Validate redirectUri from the request.
  232.      * If a redirect URI is provided ensure it matches what is pre-registered
  233.      *
  234.      * @param string                 $redirectUri
  235.      * @param ClientEntityInterface  $client
  236.      * @param ServerRequestInterface $request
  237.      *
  238.      * @throws OAuthServerException
  239.      */
  240.     protected function validateRedirectUri(
  241.         string $redirectUri,
  242.         ClientEntityInterface $client,
  243.         ServerRequestInterface $request
  244.     ) {
  245.         $validator = new RedirectUriValidator($client->getRedirectUri());
  246.         if (!$validator->validateRedirectUri($redirectUri)) {
  247.             $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED$request));
  248.             throw OAuthServerException::invalidClient($request);
  249.         }
  250.     }
  251.     /**
  252.      * Validate scopes in the request.
  253.      *
  254.      * @param string|array $scopes
  255.      * @param string       $redirectUri
  256.      *
  257.      * @throws OAuthServerException
  258.      *
  259.      * @return ScopeEntityInterface[]
  260.      */
  261.     public function validateScopes($scopes$redirectUri null)
  262.     {
  263.         if ($scopes === null) {
  264.             $scopes = [];
  265.         } elseif (\is_string($scopes)) {
  266.             $scopes $this->convertScopesQueryStringToArray($scopes);
  267.         }
  268.         if (!\is_array($scopes)) {
  269.             throw OAuthServerException::invalidRequest('scope');
  270.         }
  271.         $validScopes = [];
  272.         foreach ($scopes as $scopeItem) {
  273.             $scope $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
  274.             if ($scope instanceof ScopeEntityInterface === false) {
  275.                 throw OAuthServerException::invalidScope($scopeItem$redirectUri);
  276.             }
  277.             $validScopes[] = $scope;
  278.         }
  279.         return $validScopes;
  280.     }
  281.     /**
  282.      * Converts a scopes query string to an array to easily iterate for validation.
  283.      *
  284.      * @param string $scopes
  285.      *
  286.      * @return array
  287.      */
  288.     private function convertScopesQueryStringToArray(string $scopes)
  289.     {
  290.         return \array_filter(\explode(self::SCOPE_DELIMITER_STRING\trim($scopes)), function ($scope) {
  291.             return $scope !== '';
  292.         });
  293.     }
  294.     /**
  295.      * Retrieve request parameter.
  296.      *
  297.      * @param string                 $parameter
  298.      * @param ServerRequestInterface $request
  299.      * @param mixed                  $default
  300.      *
  301.      * @return null|string
  302.      */
  303.     protected function getRequestParameter($parameterServerRequestInterface $request$default null)
  304.     {
  305.         $requestParameters = (array) $request->getParsedBody();
  306.         return $requestParameters[$parameter] ?? $default;
  307.     }
  308.     /**
  309.      * Retrieve HTTP Basic Auth credentials with the Authorization header
  310.      * of a request. First index of the returned array is the username,
  311.      * second is the password (so list() will work). If the header does
  312.      * not exist, or is otherwise an invalid HTTP Basic header, return
  313.      * [null, null].
  314.      *
  315.      * @param ServerRequestInterface $request
  316.      *
  317.      * @return string[]|null[]
  318.      */
  319.     protected function getBasicAuthCredentials(ServerRequestInterface $request)
  320.     {
  321.         if (!$request->hasHeader('Authorization')) {
  322.             return [nullnull];
  323.         }
  324.         $header $request->getHeader('Authorization')[0];
  325.         if (\strpos($header'Basic ') !== 0) {
  326.             return [nullnull];
  327.         }
  328.         if (!($decoded \base64_decode(\substr($header6)))) {
  329.             return [nullnull];
  330.         }
  331.         if (\strpos($decoded':') === false) {
  332.             return [nullnull]; // HTTP Basic header without colon isn't valid
  333.         }
  334.         return \explode(':'$decoded2);
  335.     }
  336.     /**
  337.      * Retrieve query string parameter.
  338.      *
  339.      * @param string                 $parameter
  340.      * @param ServerRequestInterface $request
  341.      * @param mixed                  $default
  342.      *
  343.      * @return null|string
  344.      */
  345.     protected function getQueryStringParameter($parameterServerRequestInterface $request$default null)
  346.     {
  347.         return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
  348.     }
  349.     /**
  350.      * Retrieve cookie parameter.
  351.      *
  352.      * @param string                 $parameter
  353.      * @param ServerRequestInterface $request
  354.      * @param mixed                  $default
  355.      *
  356.      * @return null|string
  357.      */
  358.     protected function getCookieParameter($parameterServerRequestInterface $request$default null)
  359.     {
  360.         return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
  361.     }
  362.     /**
  363.      * Retrieve server parameter.
  364.      *
  365.      * @param string                 $parameter
  366.      * @param ServerRequestInterface $request
  367.      * @param mixed                  $default
  368.      *
  369.      * @return null|string
  370.      */
  371.     protected function getServerParameter($parameterServerRequestInterface $request$default null)
  372.     {
  373.         return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
  374.     }
  375.     /**
  376.      * Issue an access token.
  377.      *
  378.      * @param DateInterval           $accessTokenTTL
  379.      * @param ClientEntityInterface  $client
  380.      * @param string|null            $userIdentifier
  381.      * @param ScopeEntityInterface[] $scopes
  382.      *
  383.      * @throws OAuthServerException
  384.      * @throws UniqueTokenIdentifierConstraintViolationException
  385.      *
  386.      * @return AccessTokenEntityInterface
  387.      */
  388.     protected function issueAccessToken(
  389.         DateInterval $accessTokenTTL,
  390.         ClientEntityInterface $client,
  391.         $userIdentifier,
  392.         array $scopes = []
  393.     ) {
  394.         $maxGenerationAttempts self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
  395.         $accessToken $this->accessTokenRepository->getNewToken($client$scopes$userIdentifier);
  396.         $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL));
  397.         $accessToken->setPrivateKey($this->privateKey);
  398.         while ($maxGenerationAttempts-- > 0) {
  399.             $accessToken->setIdentifier($this->generateUniqueIdentifier());
  400.             try {
  401.                 $this->accessTokenRepository->persistNewAccessToken($accessToken);
  402.                 return $accessToken;
  403.             } catch (UniqueTokenIdentifierConstraintViolationException $e) {
  404.                 if ($maxGenerationAttempts === 0) {
  405.                     throw $e;
  406.                 }
  407.             }
  408.         }
  409.     }
  410.     /**
  411.      * Issue an auth code.
  412.      *
  413.      * @param DateInterval           $authCodeTTL
  414.      * @param ClientEntityInterface  $client
  415.      * @param string                 $userIdentifier
  416.      * @param string|null            $redirectUri
  417.      * @param ScopeEntityInterface[] $scopes
  418.      *
  419.      * @throws OAuthServerException
  420.      * @throws UniqueTokenIdentifierConstraintViolationException
  421.      *
  422.      * @return AuthCodeEntityInterface
  423.      */
  424.     protected function issueAuthCode(
  425.         DateInterval $authCodeTTL,
  426.         ClientEntityInterface $client,
  427.         $userIdentifier,
  428.         $redirectUri,
  429.         array $scopes = []
  430.     ) {
  431.         $maxGenerationAttempts self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
  432.         $authCode $this->authCodeRepository->getNewAuthCode();
  433.         $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL));
  434.         $authCode->setClient($client);
  435.         $authCode->setUserIdentifier($userIdentifier);
  436.         if ($redirectUri !== null) {
  437.             $authCode->setRedirectUri($redirectUri);
  438.         }
  439.         foreach ($scopes as $scope) {
  440.             $authCode->addScope($scope);
  441.         }
  442.         while ($maxGenerationAttempts-- > 0) {
  443.             $authCode->setIdentifier($this->generateUniqueIdentifier());
  444.             try {
  445.                 $this->authCodeRepository->persistNewAuthCode($authCode);
  446.                 return $authCode;
  447.             } catch (UniqueTokenIdentifierConstraintViolationException $e) {
  448.                 if ($maxGenerationAttempts === 0) {
  449.                     throw $e;
  450.                 }
  451.             }
  452.         }
  453.     }
  454.     /**
  455.      * @param AccessTokenEntityInterface $accessToken
  456.      *
  457.      * @throws OAuthServerException
  458.      * @throws UniqueTokenIdentifierConstraintViolationException
  459.      *
  460.      * @return RefreshTokenEntityInterface|null
  461.      */
  462.     protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
  463.     {
  464.         $refreshToken $this->refreshTokenRepository->getNewRefreshToken();
  465.         if ($refreshToken === null) {
  466.             return null;
  467.         }
  468.         $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
  469.         $refreshToken->setAccessToken($accessToken);
  470.         $maxGenerationAttempts self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
  471.         while ($maxGenerationAttempts-- > 0) {
  472.             $refreshToken->setIdentifier($this->generateUniqueIdentifier());
  473.             try {
  474.                 $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
  475.                 return $refreshToken;
  476.             } catch (UniqueTokenIdentifierConstraintViolationException $e) {
  477.                 if ($maxGenerationAttempts === 0) {
  478.                     throw $e;
  479.                 }
  480.             }
  481.         }
  482.     }
  483.     /**
  484.      * Generate a new unique identifier.
  485.      *
  486.      * @param int $length
  487.      *
  488.      * @throws OAuthServerException
  489.      *
  490.      * @return string
  491.      */
  492.     protected function generateUniqueIdentifier($length 40)
  493.     {
  494.         try {
  495.             return \bin2hex(\random_bytes($length));
  496.             // @codeCoverageIgnoreStart
  497.         } catch (TypeError $e) {
  498.             throw OAuthServerException::serverError('An unexpected error has occurred'$e);
  499.         } catch (Error $e) {
  500.             throw OAuthServerException::serverError('An unexpected error has occurred'$e);
  501.         } catch (Exception $e) {
  502.             // If you get this message, the CSPRNG failed hard.
  503.             throw OAuthServerException::serverError('Could not generate a random string'$e);
  504.         }
  505.         // @codeCoverageIgnoreEnd
  506.     }
  507.     /**
  508.      * {@inheritdoc}
  509.      */
  510.     public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
  511.     {
  512.         $requestParameters = (array) $request->getParsedBody();
  513.         return (
  514.             \array_key_exists('grant_type'$requestParameters)
  515.             && $requestParameters['grant_type'] === $this->getIdentifier()
  516.         );
  517.     }
  518.     /**
  519.      * {@inheritdoc}
  520.      */
  521.     public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
  522.     {
  523.         return false;
  524.     }
  525.     /**
  526.      * {@inheritdoc}
  527.      */
  528.     public function validateAuthorizationRequest(ServerRequestInterface $request)
  529.     {
  530.         throw new LogicException('This grant cannot validate an authorization request');
  531.     }
  532.     /**
  533.      * {@inheritdoc}
  534.      */
  535.     public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
  536.     {
  537.         throw new LogicException('This grant cannot complete an authorization request');
  538.     }
  539. }