Merge branch 'master' of github.com:erickjth/oauth2-server into fix-pkce-implementation

# Conflicts:
#	src/Grant/AuthCodeGrant.php
#	tests/Grant/AuthCodeGrantTest.php
This commit is contained in:
Erick Torres
2017-07-07 12:06:32 -05:00
32 changed files with 497 additions and 285 deletions

View File

@@ -3,7 +3,6 @@
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
@@ -66,6 +65,11 @@ class AuthorizationServer implements EmitterAwareInterface
*/
private $scopeRepository;
/**
* @var string
*/
private $encryptionKey;
/**
* New server instance.
*
@@ -73,7 +77,7 @@ class AuthorizationServer implements EmitterAwareInterface
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param ScopeRepositoryInterface $scopeRepository
* @param CryptKey|string $privateKey
* @param CryptKey|string $publicKey
* @param string $encryptionKey
* @param null|ResponseTypeInterface $responseType
*/
public function __construct(
@@ -81,7 +85,7 @@ class AuthorizationServer implements EmitterAwareInterface
AccessTokenRepositoryInterface $accessTokenRepository,
ScopeRepositoryInterface $scopeRepository,
$privateKey,
$publicKey,
$encryptionKey,
ResponseTypeInterface $responseType = null
) {
$this->clientRepository = $clientRepository;
@@ -93,11 +97,7 @@ class AuthorizationServer implements EmitterAwareInterface
}
$this->privateKey = $privateKey;
if ($publicKey instanceof CryptKey === false) {
$publicKey = new CryptKey($publicKey);
}
$this->publicKey = $publicKey;
$this->encryptionKey = $encryptionKey;
$this->responseType = $responseType;
}
@@ -117,8 +117,8 @@ class AuthorizationServer implements EmitterAwareInterface
$grantType->setClientRepository($this->clientRepository);
$grantType->setScopeRepository($this->scopeRepository);
$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;
@@ -200,6 +200,7 @@ class AuthorizationServer implements EmitterAwareInterface
}
$this->responseType->setPrivateKey($this->privateKey);
$this->responseType->setEncryptionKey($this->encryptionKey);
return $this->responseType;
}

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 private key
*
* @param \League\OAuth2\Server\CryptKey $key
*/
public function setPublicKey(CryptKey $key)
{
$this->publicKey = $key;
}
/**
* {@inheritdoc}
*/

View File

@@ -44,6 +44,23 @@ class CryptKey
throw new \LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath));
}
// Verify the permissions of the key
$keyPathPerms = decoct(fileperms($keyPath) & 0777);
if ($keyPathPerms !== '600') {
// Attempt to correct the permissions
if (chmod($keyPath, 0600) === false) {
// @codeCoverageIgnoreStart
throw new \LogicException(
sprintf(
'Key file "%s" permissions are not correct, should be 600 instead of %s, unable to automatically resolve the issue',
$keyPath,
$keyPathPerms
)
);
// @codeCoverageIgnoreEnd
}
}
$this->keyPath = $keyPath;
$this->passPhrase = $passPhrase;
}
@@ -57,15 +74,26 @@ 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)) {
// @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

@@ -1,47 +1,22 @@
<?php
/**
* Public/private key encryption.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use Defuse\Crypto\Crypto;
trait CryptTrait
{
/**
* @var CryptKey
* @var string
*/
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.
@@ -49,35 +24,15 @@ trait CryptTrait
* @param string $unencryptedData
*
* @throws \LogicException
*
* @return string
*/
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())
);
try {
return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey);
} catch (\Exception $e) {
throw new \LogicException($e->getMessage());
}
$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
}
$output .= $encrypted;
}
openssl_pkey_free($privateKey);
return base64_encode($output);
}
/**
@@ -86,36 +41,24 @@ trait CryptTrait
* @param string $encryptedData
*
* @throws \LogicException
*
* @return string
*/
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())
);
try {
return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey);
} catch (\Exception $e) {
throw new \LogicException($e->getMessage());
}
}
$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
}
$output .= $decrypted;
}
openssl_pkey_free($publicKey);
return $output;
/**
* Set the encryption key
*
* @param string $key
*/
public function setEncryptionKey($key = null)
{
$this->encryptionKey = $key;
}
}

View File

@@ -105,7 +105,10 @@ 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);
$hint = sprintf(
'Check the `%s` scope',
htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false)
);
return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri);
}

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,11 @@ abstract class AbstractGrant implements GrantTypeInterface
*/
protected $refreshTokenTTL;
/**
* @var \League\OAuth2\Server\CryptKey
*/
protected $privateKey;
/**
* @param ClientRepositoryInterface $clientRepository
*/
@@ -131,6 +137,16 @@ 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;
}
/**
* Validate the client.
*

View File

@@ -275,6 +275,13 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
throw OAuthServerException::invalidRequest('code_challenge');
}
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
throw OAuthServerException::invalidRequest(
'code_challenge',
'The code_challenge must be between 43 and 128 characters'
);
}
$codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
throw OAuthServerException::invalidRequest(
@@ -326,6 +333,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(
@@ -333,16 +351,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

@@ -127,9 +127,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|null $key
*/
public function setPublicKey(CryptKey $publicKey);
public function setEncryptionKey($key = null);
}

View File

@@ -102,7 +102,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

@@ -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,15 @@ 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

@@ -33,4 +33,11 @@ interface ResponseTypeInterface
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response);
/**
* Set the encryption key
*
* @param string|null $key
*/
public function setEncryptionKey($key = null);
}