Merge remote-tracking branch 'upstream/master' into exception-has-redirect

This commit is contained in:
Andrew Millington
2018-05-25 09:49:14 +01:00
68 changed files with 1764 additions and 810 deletions

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server;
use Defuse\Crypto\Key;
use League\Event\EmitterAwareInterface;
use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
@@ -17,6 +18,7 @@ use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\AbstractResponseType;
use League\OAuth2\Server\ResponseTypes\BearerTokenResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ResponseInterface;
@@ -66,6 +68,16 @@ class AuthorizationServer implements EmitterAwareInterface
*/
private $scopeRepository;
/**
* @var string|Key
*/
private $encryptionKey;
/**
* @var string
*/
private $defaultScope = '';
/**
* New server instance.
*
@@ -73,7 +85,7 @@ class AuthorizationServer implements EmitterAwareInterface
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param ScopeRepositoryInterface $scopeRepository
* @param CryptKey|string $privateKey
* @param CryptKey|string $publicKey
* @param string|Key $encryptionKey
* @param null|ResponseTypeInterface $responseType
*/
public function __construct(
@@ -81,7 +93,7 @@ class AuthorizationServer implements EmitterAwareInterface
AccessTokenRepositoryInterface $accessTokenRepository,
ScopeRepositoryInterface $scopeRepository,
$privateKey,
$publicKey,
$encryptionKey,
ResponseTypeInterface $responseType = null
) {
$this->clientRepository = $clientRepository;
@@ -92,12 +104,7 @@ class AuthorizationServer implements EmitterAwareInterface
$privateKey = new CryptKey($privateKey);
}
$this->privateKey = $privateKey;
if ($publicKey instanceof CryptKey === false) {
$publicKey = new CryptKey($publicKey);
}
$this->publicKey = $publicKey;
$this->encryptionKey = $encryptionKey;
$this->responseType = $responseType;
}
@@ -116,9 +123,10 @@ class AuthorizationServer implements EmitterAwareInterface
$grantType->setAccessTokenRepository($this->accessTokenRepository);
$grantType->setClientRepository($this->clientRepository);
$grantType->setScopeRepository($this->scopeRepository);
$grantType->setDefaultScope($this->defaultScope);
$grantType->setPrivateKey($this->privateKey);
$grantType->setPublicKey($this->publicKey);
$grantType->setEmitter($this->getEmitter());
$grantType->setEncryptionKey($this->encryptionKey);
$this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType;
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL;
@@ -172,16 +180,17 @@ class AuthorizationServer implements EmitterAwareInterface
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseInterface $response)
{
foreach ($this->enabledGrantTypes as $grantType) {
if ($grantType->canRespondToAccessTokenRequest($request)) {
$tokenResponse = $grantType->respondToAccessTokenRequest(
$request,
$this->getResponseType(),
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()]
);
if (!$grantType->canRespondToAccessTokenRequest($request)) {
continue;
}
$tokenResponse = $grantType->respondToAccessTokenRequest(
$request,
$this->getResponseType(),
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()]
);
if ($tokenResponse instanceof ResponseTypeInterface) {
return $tokenResponse->generateHttpResponse($response);
}
if ($tokenResponse instanceof ResponseTypeInterface) {
return $tokenResponse->generateHttpResponse($response);
}
}
@@ -199,8 +208,21 @@ class AuthorizationServer implements EmitterAwareInterface
$this->responseType = new BearerTokenResponse();
}
$this->responseType->setPrivateKey($this->privateKey);
if ($this->responseType instanceof AbstractResponseType === true) {
$this->responseType->setPrivateKey($this->privateKey);
}
$this->responseType->setEncryptionKey($this->encryptionKey);
return $this->responseType;
}
/**
* Set the default scope for the authorization server.
*
* @param string $defaultScope
*/
public function setDefaultScope($defaultScope)
{
$this->defaultScope = $defaultScope;
}
}

View File

@@ -12,6 +12,7 @@ namespace League\OAuth2\Server\AuthorizationValidators;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\ValidationData;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
@@ -26,6 +27,11 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
*/
private $accessTokenRepository;
/**
* @var \League\OAuth2\Server\CryptKey
*/
protected $publicKey;
/**
* @param AccessTokenRepositoryInterface $accessTokenRepository
*/
@@ -34,6 +40,16 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
$this->accessTokenRepository = $accessTokenRepository;
}
/**
* Set the public key
*
* @param \League\OAuth2\Server\CryptKey $key
*/
public function setPublicKey(CryptKey $key)
{
$this->publicKey = $key;
}
/**
* {@inheritdoc}
*/
@@ -49,8 +65,12 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
try {
// Attempt to parse and validate the JWT
$token = (new Parser())->parse($jwt);
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified');
try {
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}
} catch (\BadMethodCallException $exception) {
throw OAuthServerException::accessDenied('Access token is not signed');
}
// Ensure access token hasn't expired

View File

@@ -14,7 +14,7 @@ namespace League\OAuth2\Server;
class CryptKey
{
const RSA_KEY_PATTERN =
'/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----\n)(.|\n)+(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)$/';
'/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----)\R.*(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)\R?$/s';
/**
* @var string
@@ -29,8 +29,9 @@ class CryptKey
/**
* @param string $keyPath
* @param null|string $passPhrase
* @param bool $keyPermissionsCheck
*/
public function __construct($keyPath, $passPhrase = null)
public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true)
{
if (preg_match(self::RSA_KEY_PATTERN, $keyPath)) {
$keyPath = $this->saveKeyToFile($keyPath);
@@ -44,6 +45,18 @@ class CryptKey
throw new \LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath));
}
if ($keyPermissionsCheck === true) {
// Verify the permissions of the key
$keyPathPerms = decoct(fileperms($keyPath) & 0777);
if (in_array($keyPathPerms, ['400', '440', '600', '660'], true) === false) {
trigger_error(sprintf(
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
$keyPath,
$keyPathPerms
), E_USER_NOTICE);
}
}
$this->keyPath = $keyPath;
$this->passPhrase = $passPhrase;
}
@@ -57,15 +70,30 @@ class CryptKey
*/
private function saveKeyToFile($key)
{
$keyPath = sys_get_temp_dir() . '/' . sha1($key) . '.key';
$tmpDir = sys_get_temp_dir();
$keyPath = $tmpDir . '/' . sha1($key) . '.key';
if (!file_exists($keyPath) && !touch($keyPath)) {
if (file_exists($keyPath)) {
return 'file://' . $keyPath;
}
if (!touch($keyPath)) {
// @codeCoverageIgnoreStart
throw new \RuntimeException('"%s" key file could not be created', $keyPath);
throw new \RuntimeException(sprintf('"%s" key file could not be created', $keyPath));
// @codeCoverageIgnoreEnd
}
file_put_contents($keyPath, $key);
if (file_put_contents($keyPath, $key) === false) {
// @codeCoverageIgnoreStart
throw new \RuntimeException(sprintf('Unable to write key file to temporary directory "%s"', $tmpDir));
// @codeCoverageIgnoreEnd
}
if (chmod($keyPath, 0600) === false) {
// @codeCoverageIgnoreStart
throw new \RuntimeException(sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath));
// @codeCoverageIgnoreEnd
}
return 'file://' . $keyPath;
}

View File

@@ -11,37 +11,15 @@
namespace League\OAuth2\Server;
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
trait CryptTrait
{
/**
* @var CryptKey
* @var string|Key
*/
protected $privateKey;
/**
* @var CryptKey
*/
protected $publicKey;
/**
* Set path to private key.
*
* @param CryptKey $privateKey
*/
public function setPrivateKey(CryptKey $privateKey)
{
$this->privateKey = $privateKey;
}
/**
* Set path to public key.
*
* @param CryptKey $publicKey
*/
public function setPublicKey(CryptKey $publicKey)
{
$this->publicKey = $publicKey;
}
protected $encryptionKey;
/**
* Encrypt data with a private key.
@@ -54,30 +32,15 @@ trait CryptTrait
*/
protected function encrypt($unencryptedData)
{
$privateKey = openssl_pkey_get_private($this->privateKey->getKeyPath(), $this->privateKey->getPassPhrase());
$privateKeyDetails = @openssl_pkey_get_details($privateKey);
if ($privateKeyDetails === null) {
throw new \LogicException(
sprintf('Could not get details of private key: %s', $this->privateKey->getKeyPath())
);
}
$chunkSize = ceil($privateKeyDetails['bits'] / 8) - 11;
$output = '';
while ($unencryptedData) {
$chunk = substr($unencryptedData, 0, $chunkSize);
$unencryptedData = substr($unencryptedData, $chunkSize);
if (openssl_private_encrypt($chunk, $encrypted, $privateKey) === false) {
// @codeCoverageIgnoreStart
throw new \LogicException('Failed to encrypt data');
// @codeCoverageIgnoreEnd
try {
if ($this->encryptionKey instanceof Key) {
return Crypto::encrypt($unencryptedData, $this->encryptionKey);
}
$output .= $encrypted;
}
openssl_pkey_free($privateKey);
return base64_encode($output);
return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey);
} catch (\Exception $e) {
throw new \LogicException($e->getMessage());
}
}
/**
@@ -91,31 +54,24 @@ trait CryptTrait
*/
protected function decrypt($encryptedData)
{
$publicKey = openssl_pkey_get_public($this->publicKey->getKeyPath());
$publicKeyDetails = @openssl_pkey_get_details($publicKey);
if ($publicKeyDetails === null) {
throw new \LogicException(
sprintf('Could not get details of public key: %s', $this->publicKey->getKeyPath())
);
}
$chunkSize = ceil($publicKeyDetails['bits'] / 8);
$output = '';
$encryptedData = base64_decode($encryptedData);
while ($encryptedData) {
$chunk = substr($encryptedData, 0, $chunkSize);
$encryptedData = substr($encryptedData, $chunkSize);
if (openssl_public_decrypt($chunk, $decrypted, $publicKey/*, OPENSSL_PKCS1_OAEP_PADDING*/) === false) {
// @codeCoverageIgnoreStart
throw new \LogicException('Failed to decrypt data');
// @codeCoverageIgnoreEnd
try {
if ($this->encryptionKey instanceof Key) {
return Crypto::decrypt($encryptedData, $this->encryptionKey);
}
$output .= $decrypted;
}
openssl_pkey_free($publicKey);
return $output;
return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey);
} catch (\Exception $e) {
throw new \LogicException($e->getMessage());
}
}
/**
* Set the encryption key
*
* @param string|Key $key
*/
public function setEncryptionKey($key = null)
{
$this->encryptionKey = $key;
}
}

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Entities;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKey;
interface AccessTokenEntityInterface extends TokenInterface
@@ -18,7 +19,7 @@ interface AccessTokenEntityInterface extends TokenInterface
*
* @param CryptKey $privateKey
*
* @return string
* @return Token
*/
public function convertToJWT(CryptKey $privateKey);
}

View File

@@ -12,7 +12,7 @@ namespace League\OAuth2\Server\Entities;
interface AuthCodeEntityInterface extends TokenInterface
{
/**
* @return string
* @return string|null
*/
public function getRedirectUri();

View File

@@ -21,7 +21,7 @@ interface RefreshTokenEntityInterface
/**
* Set the token's identifier.
*
* @param $identifier
* @param mixed $identifier
*/
public function setIdentifier($identifier);

View File

@@ -21,7 +21,7 @@ interface TokenInterface
/**
* Set the token's identifier.
*
* @param $identifier
* @param mixed $identifier
*/
public function setIdentifier($identifier);
@@ -42,14 +42,14 @@ interface TokenInterface
/**
* Set the identifier of the user associated with the token.
*
* @param string|int $identifier The identifier of the user
* @param string|int|null $identifier The identifier of the user
*/
public function setUserIdentifier($identifier);
/**
* Get the token user's identifier.
*
* @return string|int
* @return string|int|null
*/
public function getUserIdentifier();

View File

@@ -12,6 +12,7 @@ namespace League\OAuth2\Server\Entities\Traits;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
@@ -23,7 +24,7 @@ trait AccessTokenTrait
*
* @param CryptKey $privateKey
*
* @return string
* @return Token
*/
public function convertToJWT(CryptKey $privateKey)
{

View File

@@ -17,7 +17,7 @@ trait AuthCodeTrait
protected $redirectUri;
/**
* @return string
* @return string|null
*/
public function getRedirectUri()
{

View File

@@ -11,7 +11,7 @@ namespace League\OAuth2\Server\Entities\Traits;
trait EntityTrait
{
/*
/**
* @var string
*/
protected $identifier;

View File

@@ -25,7 +25,7 @@ trait TokenEntityTrait
protected $expiryDateTime;
/**
* @var string|int
* @var string|int|null
*/
protected $userIdentifier;
@@ -77,7 +77,7 @@ trait TokenEntityTrait
/**
* Set the identifier of the user associated with the token.
*
* @param string|int $identifier The identifier of the user
* @param string|int|null $identifier The identifier of the user
*/
public function setUserIdentifier($identifier)
{
@@ -87,7 +87,7 @@ trait TokenEntityTrait
/**
* Get the token user's identifier.
*
* @return string|int
* @return string|int|null
*/
public function getUserIdentifier()
{

View File

@@ -33,6 +33,11 @@ class OAuthServerException extends \Exception
*/
private $redirectUri;
/**
* @var array
*/
private $payload;
/**
* Throw a new exception.
*
@@ -50,6 +55,33 @@ class OAuthServerException extends \Exception
$this->errorType = $errorType;
$this->hint = $hint;
$this->redirectUri = $redirectUri;
$this->payload = [
'error' => $errorType,
'message' => $message,
];
if ($hint !== null) {
$this->payload['hint'] = $hint;
}
}
/**
* Returns the current payload.
*
* @return array
*/
public function getPayload()
{
return $this->payload;
}
/**
* Updates the current payload.
*
* @param array $payload
*/
public function setPayload(array $payload)
{
$this->payload = $payload;
}
/**
@@ -60,7 +92,7 @@ class OAuthServerException extends \Exception
public static function unsupportedGrantType()
{
$errorMessage = 'The authorization grant type is not supported by the authorization server.';
$hint = 'Check the `grant_type` parameter';
$hint = 'Check that all required parameters have been provided';
return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint);
}
@@ -105,7 +137,15 @@ class OAuthServerException extends \Exception
public static function invalidScope($scope, $redirectUri = null)
{
$errorMessage = 'The requested scope is invalid, unknown, or malformed';
$hint = sprintf('Check the `%s` scope', $scope);
if (empty($scope)) {
$hint = 'Specify a scope in the request or set a default scope';
} else {
$hint = sprintf(
'Check the `%s` scope',
htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false)
);
}
return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri);
}
@@ -123,7 +163,7 @@ class OAuthServerException extends \Exception
/**
* Server error.
*
* @param $hint
* @param string $hint
*
* @return static
*
@@ -149,7 +189,7 @@ class OAuthServerException extends \Exception
*/
public static function invalidRefreshToken($hint = null)
{
return new static('The refresh token is invalid.', 8, 'invalid_request', 400, $hint);
return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint);
}
/**
@@ -205,21 +245,15 @@ class OAuthServerException extends \Exception
*
* @param ResponseInterface $response
* @param bool $useFragment True if errors should be in the URI fragment instead of query string
* @param int $jsonOptions options passed to json_encode
*
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response, $useFragment = false)
public function generateHttpResponse(ResponseInterface $response, $useFragment = false, $jsonOptions = 0)
{
$headers = $this->getHttpHeaders();
$payload = [
'error' => $this->getErrorType(),
'message' => $this->getMessage(),
];
if ($this->hint !== null) {
$payload['hint'] = $this->hint;
}
$payload = $this->getPayload();
if ($this->redirectUri !== null) {
if ($useFragment === true) {
@@ -235,7 +269,7 @@ class OAuthServerException extends \Exception
$response = $response->withHeader($header, $content);
}
$response->getBody()->write(json_encode($payload));
$response->getBody()->write(json_encode($payload, $jsonOptions));
return $response->withStatus($this->getHttpStatusCode());
}
@@ -260,13 +294,9 @@ class OAuthServerException extends \Exception
// include the "WWW-Authenticate" response header field
// matching the authentication scheme used by the client.
// @codeCoverageIgnoreStart
if ($this->errorType === 'invalid_client') {
$authScheme = 'Basic';
if (array_key_exists('HTTP_AUTHORIZATION', $_SERVER) !== false
&& strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer') === 0
) {
$authScheme = 'Bearer';
}
if ($this->errorType === 'invalid_client' && array_key_exists('HTTP_AUTHORIZATION', $_SERVER) !== false) {
$authScheme = strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer') === 0 ? 'Bearer' : 'Basic';
$headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"';
}
// @codeCoverageIgnoreEnd

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\Grant;
use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
@@ -75,6 +76,16 @@ abstract class AbstractGrant implements GrantTypeInterface
*/
protected $refreshTokenTTL;
/**
* @var \League\OAuth2\Server\CryptKey
*/
protected $privateKey;
/**
* @string
*/
protected $defaultScope;
/**
* @param ClientRepositoryInterface $clientRepository
*/
@@ -131,6 +142,24 @@ abstract class AbstractGrant implements GrantTypeInterface
$this->refreshTokenTTL = $refreshTokenTTL;
}
/**
* Set the private key
*
* @param \League\OAuth2\Server\CryptKey $key
*/
public function setPrivateKey(CryptKey $key)
{
$this->privateKey = $key;
}
/**
* @param string $scope
*/
public function setDefaultScope($scope)
{
$this->defaultScope = $scope;
}
/**
* Validate the client.
*
@@ -175,7 +204,7 @@ abstract class AbstractGrant implements GrantTypeInterface
throw OAuthServerException::invalidClient();
} elseif (
is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri()) === false
&& in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
@@ -195,18 +224,14 @@ abstract class AbstractGrant implements GrantTypeInterface
*
* @return ScopeEntityInterface[]
*/
public function validateScopes(
$scopes,
$redirectUri = null
) {
$scopesList = array_filter(
explode(self::SCOPE_DELIMITER_STRING, trim($scopes)),
function ($scope) {
return !empty($scope);
}
);
public function validateScopes($scopes, $redirectUri = null)
{
$scopesList = array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) {
return !empty($scope);
});
$validScopes = [];
$scopes = [];
foreach ($scopesList as $scopeItem) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
@@ -214,10 +239,10 @@ abstract class AbstractGrant implements GrantTypeInterface
throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
}
$scopes[] = $scope;
$validScopes[] = $scope;
}
return $scopes;
return $validScopes;
}
/**
@@ -316,7 +341,7 @@ abstract class AbstractGrant implements GrantTypeInterface
*
* @param \DateInterval $accessTokenTTL
* @param ClientEntityInterface $client
* @param string $userIdentifier
* @param string|null $userIdentifier
* @param ScopeEntityInterface[] $scopes
*
* @throws OAuthServerException
@@ -361,7 +386,7 @@ abstract class AbstractGrant implements GrantTypeInterface
* @param \DateInterval $authCodeTTL
* @param ClientEntityInterface $client
* @param string $userIdentifier
* @param string $redirectUri
* @param string|null $redirectUri
* @param ScopeEntityInterface[] $scopes
*
* @throws OAuthServerException
@@ -382,7 +407,10 @@ abstract class AbstractGrant implements GrantTypeInterface
$authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL));
$authCode->setClient($client);
$authCode->setUserIdentifier($userIdentifier);
$authCode->setRedirectUri($redirectUri);
if ($redirectUri !== null) {
$authCode->setRedirectUri($redirectUri);
}
foreach ($scopes as $scope) {
$authCode->addScope($scope);

View File

@@ -134,6 +134,15 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
throw OAuthServerException::invalidRequest('code_verifier');
}
// Validate code_verifier according to RFC-7636
// @see: https://tools.ietf.org/html/rfc7636#section-4.1
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
throw OAuthServerException::invalidRequest(
'code_verifier',
'Code Verifier must follow the specifications of RFC-7636.'
);
}
switch ($authCodePayload->code_challenge_method) {
case 'plain':
if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
@@ -144,7 +153,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
case 'S256':
if (
hash_equals(
urlencode(base64_encode(hash('sha256', $codeVerifier))),
strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
$authCodePayload->code_challenge
) === false
) {
@@ -167,6 +176,10 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
// Inject tokens into response type
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -209,6 +222,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$request,
$this->getServerParameter('PHP_AUTH_USER', $request)
);
if (is_null($clientId)) {
throw OAuthServerException::invalidRequest('client_id');
}
@@ -226,6 +240,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
}
$redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
if ($redirectUri !== null) {
if (
is_string($client->getRedirectUri())
@@ -235,18 +250,24 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
throw OAuthServerException::invalidClient();
} elseif (
is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri()) === false
&& in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
} elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
|| empty($client->getRedirectUri())) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
} else {
$redirectUri = is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
}
$scopes = $this->validateScopes(
$this->getQueryStringParameter('scope', $request),
is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri()
$this->getQueryStringParameter('scope', $request, $this->defaultScope),
$redirectUri
);
$stateParameter = $this->getQueryStringParameter('state', $request);
@@ -255,7 +276,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
$authorizationRequest->setRedirectUri($redirectUri);
$authorizationRequest->setState($stateParameter);
if ($stateParameter !== null) {
$authorizationRequest->setState($stateParameter);
}
$authorizationRequest->setScopes($scopes);
if ($this->enableCodeExchangeProof === true) {
@@ -265,13 +290,23 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
}
$codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
if (in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) {
throw OAuthServerException::invalidRequest(
'code_challenge_method',
'Code challenge method must be `plain` or `S256`'
);
}
// Validate code_challenge according to RFC-7636
// @see: https://tools.ietf.org/html/rfc7636#section-4.2
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
throw OAuthServerException::invalidRequest(
'code_challenged',
'Code challenge must follow the specifications of RFC-7636.'
);
}
$authorizationRequest->setCodeChallenge($codeChallenge);
$authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
}
@@ -304,6 +339,17 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$authorizationRequest->getScopes()
);
$payload = [
'client_id' => $authCode->getClient()->getIdentifier(),
'redirect_uri' => $authCode->getRedirectUri(),
'auth_code_id' => $authCode->getIdentifier(),
'scopes' => $authCode->getScopes(),
'user_id' => $authCode->getUserIdentifier(),
'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'),
'code_challenge' => $authorizationRequest->getCodeChallenge(),
'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
];
$response = new RedirectResponse();
$response->setRedirectUri(
$this->makeRedirectUri(
@@ -311,16 +357,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
[
'code' => $this->encrypt(
json_encode(
[
'client_id' => $authCode->getClient()->getIdentifier(),
'redirect_uri' => $authCode->getRedirectUri(),
'auth_code_id' => $authCode->getIdentifier(),
'scopes' => $authCode->getScopes(),
'user_id' => $authCode->getUserIdentifier(),
'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'),
'code_challenge' => $authorizationRequest->getCodeChallenge(),
'code_challenge_method ' => $authorizationRequest->getCodeChallengeMethod(),
]
$payload
)
),
'state' => $authorizationRequest->getState(),

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\Grant;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -29,13 +30,16 @@ class ClientCredentialsGrant extends AbstractGrant
) {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);
// Issue and persist access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $scopes);
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);
// Send event to emitter
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
// Inject access token into response type
$responseType->setAccessToken($accessToken);

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\Grant;
use Defuse\Crypto\Key;
use League\Event\EmitterAwareInterface;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
@@ -119,6 +120,13 @@ interface GrantTypeInterface extends EmitterAwareInterface
*/
public function setScopeRepository(ScopeRepositoryInterface $scopeRepository);
/**
* Set the default scope.
*
* @param string $scope
*/
public function setDefaultScope($scope);
/**
* Set the path to the private key.
*
@@ -127,9 +135,9 @@ interface GrantTypeInterface extends EmitterAwareInterface
public function setPrivateKey(CryptKey $privateKey);
/**
* Set the path to the public key.
* Set the encryption key
*
* @param CryptKey $publicKey
* @param string|Key|null $key
*/
public function setPublicKey(CryptKey $publicKey);
public function setEncryptionKey($key = null);
}

View File

@@ -27,11 +27,18 @@ class ImplicitGrant extends AbstractAuthorizeGrant
private $accessTokenTTL;
/**
* @param \DateInterval $accessTokenTTL
* @var string
*/
public function __construct(\DateInterval $accessTokenTTL)
private $queryDelimiter;
/**
* @param \DateInterval $accessTokenTTL
* @param string $queryDelimiter
*/
public function __construct(\DateInterval $accessTokenTTL, $queryDelimiter = '#')
{
$this->accessTokenTTL = $accessTokenTTL;
$this->queryDelimiter = $queryDelimiter;
}
/**
@@ -95,7 +102,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant
public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
{
return (
array_key_exists('response_type', $request->getQueryParams())
isset($request->getQueryParams()['response_type'])
&& $request->getQueryParams()['response_type'] === 'token'
&& isset($request->getQueryParams()['client_id'])
);
@@ -137,22 +144,28 @@ class ImplicitGrant extends AbstractAuthorizeGrant
throw OAuthServerException::invalidClient();
} elseif (
is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri()) === false
&& in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
} elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
|| empty($client->getRedirectUri())) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
} else {
$redirectUri = is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
}
$scopes = $this->validateScopes(
$this->getQueryStringParameter('scope', $request),
is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri()
$this->getQueryStringParameter('scope', $request, $this->defaultScope),
$redirectUri
);
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes(
$finalizedScopes = $this->scopeRepository->finalizeScopes(
$scopes,
$this->getIdentifier(),
$client
@@ -164,8 +177,12 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
$authorizationRequest->setRedirectUri($redirectUri);
$authorizationRequest->setState($stateParameter);
$authorizationRequest->setScopes($scopes);
if ($stateParameter !== null) {
$authorizationRequest->setState($stateParameter);
}
$authorizationRequest->setScopes($finalizedScopes);
return $authorizationRequest;
}
@@ -200,11 +217,11 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$finalRedirectUri,
[
'access_token' => (string) $accessToken->convertToJWT($this->privateKey),
'token_type' => 'bearer',
'token_type' => 'Bearer',
'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - (new \DateTime())->getTimestamp(),
'state' => $authorizationRequest->getState(),
],
'#'
$this->queryDelimiter
)
);

View File

@@ -49,16 +49,20 @@ class PasswordGrant extends AbstractGrant
) {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
$user = $this->validateUser($request, $client);
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes);
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);

View File

@@ -11,7 +11,6 @@
namespace League\OAuth2\Server\Grant;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
@@ -44,28 +43,17 @@ class RefreshTokenGrant extends AbstractGrant
// Validate request
$client = $this->validateClient($request);
$oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier());
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$scopes = $this->validateScopes($this->getRequestParameter(
'scope',
$request,
implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes']))
);
// If no new scopes are requested then give the access token the original session scopes
if (count($scopes) === 0) {
$scopes = array_map(function ($scopeId) use ($client) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
if ($scope instanceof ScopeEntityInterface === false) {
// @codeCoverageIgnoreStart
throw OAuthServerException::invalidScope($scopeId);
// @codeCoverageIgnoreEnd
}
return $scope;
}, $oldRefreshToken['scopes']);
} else {
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($scopes as $scope) {
if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes']) === false) {
throw OAuthServerException::invalidScope($scope->getIdentifier());
}
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($scopes as $scope) {
if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) {
throw OAuthServerException::invalidScope($scope->getIdentifier());
}
}
@@ -77,6 +65,10 @@ class RefreshTokenGrant extends AbstractGrant
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -102,7 +94,7 @@ class RefreshTokenGrant extends AbstractGrant
// Validate refresh token
try {
$refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (\LogicException $e) {
} catch (\Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
}

View File

@@ -12,6 +12,7 @@ namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
/**
* Access token interface.
@@ -33,6 +34,8 @@ interface AccessTokenRepositoryInterface extends RepositoryInterface
* Persists a new access token to permanent storage.
*
* @param AccessTokenEntityInterface $accessTokenEntity
*
* @throws UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity);

View File

@@ -10,6 +10,7 @@
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
/**
* Auth code storage interface.
@@ -27,6 +28,8 @@ interface AuthCodeRepositoryInterface extends RepositoryInterface
* Persists a new auth code to permanent storage.
*
* @param AuthCodeEntityInterface $authCodeEntity
*
* @throws UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity);

View File

@@ -20,12 +20,12 @@ interface ClientRepositoryInterface extends RepositoryInterface
* Get a client.
*
* @param string $clientIdentifier The client's identifier
* @param string $grantType The grant type used
* @param null|string $grantType The grant type used (if sent)
* @param null|string $clientSecret The client's secret (if sent)
* @param bool $mustValidateSecret If true the client must attempt to validate the secret if the client
* is confidential
*
* @return ClientEntityInterface
*/
public function getClientEntity($clientIdentifier, $grantType, $clientSecret = null, $mustValidateSecret = true);
public function getClientEntity($clientIdentifier, $grantType = null, $clientSecret = null, $mustValidateSecret = true);
}

View File

@@ -10,6 +10,7 @@
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
/**
* Refresh token interface.
@@ -27,6 +28,8 @@ interface RefreshTokenRepositoryInterface extends RepositoryInterface
* Create a new refresh token_name.
*
* @param RefreshTokenEntityInterface $refreshTokenEntity
*
* @throws UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity);

View File

@@ -18,6 +18,9 @@ class RequestEvent extends Event
const USER_AUTHENTICATION_FAILED = 'user.authentication.failed';
const REFRESH_TOKEN_CLIENT_FAILED = 'refresh_token.client.failed';
const REFRESH_TOKEN_ISSUED = 'refresh_token.issued';
const ACCESS_TOKEN_ISSUED = 'access_token.issued';
/**
* @var ServerRequestInterface
*/

View File

@@ -53,14 +53,14 @@ class AuthorizationRequest
/**
* The redirect URI used in the request
*
* @var string
* @var string|null
*/
protected $redirectUri;
/**
* The state parameter on the authorization request
*
* @var string
* @var string|null
*/
protected $state;
@@ -159,7 +159,7 @@ class AuthorizationRequest
}
/**
* @return string
* @return string|null
*/
public function getRedirectUri()
{
@@ -167,7 +167,7 @@ class AuthorizationRequest
}
/**
* @param string $redirectUri
* @param string|null $redirectUri
*/
public function setRedirectUri($redirectUri)
{
@@ -175,7 +175,7 @@ class AuthorizationRequest
}
/**
* @return string
* @return string|null
*/
public function getState()
{

View File

@@ -63,7 +63,9 @@ class ResourceServer
$this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository);
}
$this->authorizationValidator->setPublicKey($this->publicKey);
if ($this->authorizationValidator instanceof BearerTokenValidator === true) {
$this->authorizationValidator->setPublicKey($this->publicKey);
}
return $this->authorizationValidator;
}

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\ResponseTypes;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
@@ -29,6 +30,11 @@ abstract class AbstractResponseType implements ResponseTypeInterface
*/
protected $refreshToken;
/**
* @var CryptKey
*/
protected $privateKey;
/**
* {@inheritdoc}
*/
@@ -44,4 +50,14 @@ abstract class AbstractResponseType implements ResponseTypeInterface
{
$this->refreshToken = $refreshToken;
}
/**
* Set the private key
*
* @param \League\OAuth2\Server\CryptKey $key
*/
public function setPrivateKey(CryptKey $key)
{
$this->privateKey = $key;
}
}

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\ResponseTypes;
use Defuse\Crypto\Key;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use Psr\Http\Message\ResponseInterface;
@@ -33,4 +34,11 @@ interface ResponseTypeInterface
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response);
/**
* Set the encryption key
*
* @param string|Key|null $key
*/
public function setEncryptionKey($key = null);
}