Skip to content

Commit

Permalink
Merge master into branch
Browse files Browse the repository at this point in the history
  • Loading branch information
Sephster committed Jan 17, 2022
2 parents 46b3cb1 + aab8994 commit de61d4e
Show file tree
Hide file tree
Showing 31 changed files with 900 additions and 128 deletions.
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
time: "11:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: league/event
versions:
- 3.0.0
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
run: vendor/bin/phpunit --verbose --coverage-clover=coverage.clover

- name: Code coverage
if: ${{ github.ref == 'refs/heads/master' && matrix.php != 8.0 }}
if: ${{ github.ref == 'refs/heads/master' && matrix.php != 8.0 && github.repository == 'thephpleague/oauth2-server' }}
run: |
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/vendor
/composer.lock
phpunit.xml
.phpunit.result.cache
.idea
/examples/vendor
examples/public.key
Expand Down
45 changes: 41 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,50 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added (v9)
### Added
- A CryptKeyInterface to allow developers to change the CryptKey implementation with greater ease (PR #1044)
- The authorization server can now finalize scopes when a client uses a refresh token (PR #1094)
- An AuthorizationRequestInterface to make it easier to extend the AuthorizationRequest (PR #1110)

### Fixed (v9)
### Fixed
- If a refresh token has expired, been revoked, cannot be decrypted, or does not belong to the correct client, the server will now issue an `invalid_grant` error and a HTTP 400 response. In previous versions the server incorrectly issued an `invalid_request` and HTTP 401 response (PR #1042) (PR #1082)

### Changed (v9)
### Changed
- Authorization Request objects are now created through the factory method, `createAuthorizationRequest()` (PR #1111)
- Changed parameters for `finalizeScopes()` to allow a reference to an auth code ID (PR #1112)

## [8.3.3] - released 2021-10-11
### Security
- Removed the use of `LocalFileReference()` in lcobucci/jwt. Function deprecated as per [GHSA-7322-jrq4-x5hf](https://github.com/lcobucci/jwt/security/advisories/GHSA-7322-jrq4-x5hf) (PR #1249)

## [8.3.2] - released 2021-07-27
### Changed
- Conditionally support the `StrictValidAt()` method in lcobucci/jwt so we can use version 4.1.x or greater of the library (PR #1236)
- When providing invalid credentials, the library now responds with the error message _The user credentials were incorrect_ (PR #1230)
- Keys are always stored in memory now and are not written to a file in the /tmp directory (PR #1180)
- The regex for matching the bearer token has been simplified (PR #1238)

## [8.3.1] - released 2021-06-04
### Fixed
- Revert check on clientID. We will no longer require this to be a string (PR #1233)

## [8.3.0] - released 2021-06-03
### Added
- The server will now validate redirect uris according to rfc8252 (PR #1203)
- Events emitted now include the refresh token and access token payloads (PR #1211)
- Use the `revokeRefreshTokens()` function to decide whether refresh tokens are revoked or not upon use (PR #1189)

### Changed
- Keys are now validated using `openssl_pkey_get_private()` and `openssl_pkey_get_public()` instead of regex matching (PR #1215)

### Fixed
- The server will now only recognise and handle an authorization header if the value of the header is non-empty. This is to circumvent issues where some common frameworks set this header even if no value is present (PR #1170)
- Added type validation for redirect uri, client ID, client secret, scopes, auth code, state, username, and password inputs (PR #1210)
- Allow scope "0" to be used. Previously this was removed from a request because it failed an `empty()` check (PR #1181)

## [8.2.4] - released 2020-12-10
### Fixed
- Reverted the enforcement of at least one redirect_uri for a client. This change has instead been moved to version 9 (PR #1169)

## [8.2.3] - released 2020-12-02
### Added
Expand Down Expand Up @@ -534,7 +566,12 @@ Version 5 is a complete code rewrite.

- First major release

[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.2.3...HEAD
[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.3.3...HEAD
[8.3.3]: https://github.com/thephpleague/oauth2-server/compare/8.3.2...8.3.3
[8.3.2]: https://github.com/thephpleague/oauth2-server/compare/8.3.1...8.3.2
[8.3.1]: https://github.com/thephpleague/oauth2-server/compare/8.3.0...8.3.1
[8.3.0]: https://github.com/thephpleague/oauth2-server/compare/8.2.4...8.3.0
[8.2.4]: https://github.com/thephpleague/oauth2-server/compare/8.2.3...8.2.4
[8.2.3]: https://github.com/thephpleague/oauth2-server/compare/8.2.2...8.2.3
[8.2.2]: https://github.com/thephpleague/oauth2-server/compare/8.2.1...8.2.2
[8.2.1]: https://github.com/thephpleague/oauth2-server/compare/8.2.0...8.2.1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ We use [Github Actions](https://github.com/features/actions), [Scrutinizer](http
* [Laravel Passport](https://github.com/laravel/passport)
* [OAuth 2 Server for CakePHP 3](https://github.com/uafrica/oauth-server)
* [OAuth 2 Server for Mezzio](https://github.com/mezzio/mezzio-authentication-oauth2)
* [Trikoder OAuth 2 Bundle (Symfony)](https://github.com/trikoder/oauth2-bundle)
* [OAuth 2 Server Bundle (Symfony)](https://github.com/thephpleague/oauth2-server-bundle)
* [Heimdall for CodeIgniter 4](https://github.com/ezralazuardy/heimdall)

## Changelog
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"php": "^7.2 || ^8.0",
"ext-openssl": "*",
"league/event": "^2.2",
"lcobucci/jwt": "^3.4 || ^4.0",
"lcobucci/jwt": "^3.4.6 || ^4.0.4",
"psr/http-message": "^1.0.1",
"defuse/php-encryption": "^2.2.1",
"ext-json": "*"
Expand Down
2 changes: 1 addition & 1 deletion examples/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
},
"require-dev": {
"league/event": "^2.2",
"lcobucci/jwt": "^3.4 || ^4.0",
"lcobucci/jwt": "^3.4.6 || ^4.0.4",
"psr/http-message": "^1.0.1",
"defuse/php-encryption": "^2.2.1",
"laminas/laminas-diactoros": "^2.5.0"
Expand Down
16 changes: 16 additions & 0 deletions src/AuthorizationServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class AuthorizationServer implements EmitterAwareInterface
*/
private $defaultScope = '';

/**
* @var bool
*/
private $revokeRefreshTokens = true;

/**
* New server instance.
*
Expand Down Expand Up @@ -136,6 +141,7 @@ public function enableGrantType(GrantTypeInterface $grantType, DateInterval $acc
$grantType->setPrivateKey($this->privateKey);
$grantType->setEmitter($this->getEmitter());
$grantType->setEncryptionKey($this->encryptionKey);
$grantType->revokeRefreshTokens($this->revokeRefreshTokens);

$this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType;
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL;
Expand Down Expand Up @@ -235,4 +241,14 @@ public function setDefaultScope($defaultScope)
{
$this->defaultScope = $defaultScope;
}

/**
* Sets whether to revoke refresh tokens or not (for all grant types).
*
* @param bool $revokeRefreshTokens
*/
public function revokeRefreshTokens(bool $revokeRefreshTokens): void
{
$this->revokeRefreshTokens = $revokeRefreshTokens;
}
}
34 changes: 18 additions & 16 deletions src/AuthorizationValidators/BearerTokenValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@
use DateTimeZone;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Encoding\CannotDecodeContent;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Token\UnsupportedHeaderFound;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use League\OAuth2\Server\CryptKeyInterface;
Expand Down Expand Up @@ -77,8 +74,13 @@ private function initJwtConfiguration()
);

$this->jwtConfiguration->setValidationConstraints(
new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))),
new SignedWith(new Sha256(), LocalFileReference::file($this->publicKey->getKeyPath()))
\class_exists(StrictValidAt::class)
? new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get())))
: new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))),
new SignedWith(
new Sha256(),
InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '')
)
);
}

Expand All @@ -92,21 +94,21 @@ public function validateAuthorization(ServerRequestInterface $request)
}

$header = $request->getHeader('authorization');
$jwt = \trim((string) \preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
$jwt = \trim((string) \preg_replace('/^\s*Bearer\s/', '', $header[0]));

try {
// Attempt to parse and validate the JWT
// Attempt to parse the JWT
$token = $this->jwtConfiguration->parser()->parse($jwt);
} catch (\Lcobucci\JWT\Exception $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
}

try {
// Attempt to validate the JWT
$constraints = $this->jwtConfiguration->validationConstraints();

try {
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $exception) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}
} catch (CannotDecodeContent | InvalidTokenStructure | UnsupportedHeaderFound $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $exception) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}

$claims = $token->claims();
Expand Down
103 changes: 60 additions & 43 deletions src/CryptKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@
namespace League\OAuth2\Server;

use LogicException;
use RuntimeException;

class CryptKey implements CryptKeyInterface
{
/** @deprecated left for backward compatibility check */
const RSA_KEY_PATTERN =
'/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----)\R.*(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)\R?$/s';

private const FILE_PREFIX = 'file://';

/**
* @var string Key contents
*/
protected $keyContents;

/**
* @var string
*/
Expand All @@ -36,67 +43,77 @@ class CryptKey implements CryptKeyInterface
*/
public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true)
{
if ($rsaMatch = \preg_match(static::RSA_KEY_PATTERN, $keyPath)) {
$keyPath = $this->saveKeyToFile($keyPath);
} elseif ($rsaMatch === false) {
throw new \RuntimeException(
\sprintf('PCRE error [%d] encountered during key match attempt', \preg_last_error())
);
}
$this->passPhrase = $passPhrase;

if (\strpos($keyPath, 'file://') !== 0) {
$keyPath = 'file://' . $keyPath;
}
if (\strpos($keyPath, self::FILE_PREFIX) !== 0 && $this->isValidKey($keyPath, $this->passPhrase ?? '')) {
$this->keyContents = $keyPath;
$this->keyPath = '';
// There's no file, so no need for permission check.
$keyPermissionsCheck = false;
} elseif (\is_file($keyPath)) {
if (\strpos($keyPath, self::FILE_PREFIX) !== 0) {
$keyPath = self::FILE_PREFIX . $keyPath;
}

if (!\file_exists($keyPath) || !\is_readable($keyPath)) {
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
if (!\is_readable($keyPath)) {
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
}
$this->keyContents = \file_get_contents($keyPath);
$this->keyPath = $keyPath;
if (!$this->isValidKey($this->keyContents, $this->passPhrase ?? '')) {
throw new LogicException('Unable to read key from file ' . $keyPath);
}
} else {
throw new LogicException('Unable to read key from file ' . $keyPath);
}

if ($keyPermissionsCheck === true) {
// Verify the permissions of the key
$keyPathPerms = \decoct(\fileperms($keyPath) & 0777);
$keyPathPerms = \decoct(\fileperms($this->keyPath) & 0777);
if (\in_array($keyPathPerms, ['400', '440', '600', '640', '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);
\trigger_error(
\sprintf(
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
$this->keyPath,
$keyPathPerms
),
E_USER_NOTICE
);
}
}
}

$this->keyPath = $keyPath;
$this->passPhrase = $passPhrase;
/**
* Get key contents
*
* @return string Key contents
*/
public function getKeyContents(): string
{
return $this->keyContents;
}

/**
* @param string $key
* Validate key contents.
*
* @throws RuntimeException
* @param string $contents
* @param string $passPhrase
*
* @return string
* @return bool
*/
private function saveKeyToFile($key)
private function isValidKey($contents, $passPhrase)
{
$tmpDir = \sys_get_temp_dir();
$keyPath = $tmpDir . '/' . \sha1($key) . '.key';

if (\file_exists($keyPath)) {
return 'file://' . $keyPath;
}

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
$pkey = \openssl_pkey_get_private($contents, $passPhrase) ?: \openssl_pkey_get_public($contents);
if ($pkey === false) {
return false;
}
$details = \openssl_pkey_get_details($pkey);

return 'file://' . $keyPath;
return $details !== false && \in_array(
$details['type'] ?? -1,
[OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC],
true
);
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/Entities/Traits/AccessTokenTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use DateTimeImmutable;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKeyInterface;
Expand Down Expand Up @@ -46,7 +45,7 @@ public function initJwtConfiguration()
{
$this->jwtConfiguration = Configuration::forAsymmetricSigner(
new Sha256(),
LocalFileReference::file($this->privateKey->getKeyPath(), $this->privateKey->getPassPhrase() ?? ''),
InMemory::plainText($this->privateKey->getKeyContents(), $this->privateKey->getPassPhrase() ?? ''),
InMemory::plainText('')
);
}
Expand Down
Loading

0 comments on commit de61d4e

Please sign in to comment.