Skip to content

Commit de61d4e

Browse files
committed
Merge master into branch
2 parents 46b3cb1 + aab8994 commit de61d4e

31 files changed

+900
-128
lines changed

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: composer
4+
directory: "/"
5+
schedule:
6+
interval: daily
7+
time: "11:00"
8+
open-pull-requests-limit: 10
9+
ignore:
10+
- dependency-name: league/event
11+
versions:
12+
- 3.0.0

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
run: vendor/bin/phpunit --verbose --coverage-clover=coverage.clover
3737

3838
- name: Code coverage
39-
if: ${{ github.ref == 'refs/heads/master' && matrix.php != 8.0 }}
39+
if: ${{ github.ref == 'refs/heads/master' && matrix.php != 8.0 && github.repository == 'thephpleague/oauth2-server' }}
4040
run: |
4141
wget https://scrutinizer-ci.com/ocular.phar
4242
php ocular.phar code-coverage:upload --format=php-clover coverage.clover

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/vendor
22
/composer.lock
33
phpunit.xml
4+
.phpunit.result.cache
45
.idea
56
/examples/vendor
67
examples/public.key

CHANGELOG.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,50 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

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

13-
### Fixed (v9)
13+
### Fixed
1414
- 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)
1515

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

20+
## [8.3.3] - released 2021-10-11
21+
### Security
22+
- 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)
23+
24+
## [8.3.2] - released 2021-07-27
25+
### Changed
26+
- Conditionally support the `StrictValidAt()` method in lcobucci/jwt so we can use version 4.1.x or greater of the library (PR #1236)
27+
- When providing invalid credentials, the library now responds with the error message _The user credentials were incorrect_ (PR #1230)
28+
- Keys are always stored in memory now and are not written to a file in the /tmp directory (PR #1180)
29+
- The regex for matching the bearer token has been simplified (PR #1238)
30+
31+
## [8.3.1] - released 2021-06-04
32+
### Fixed
33+
- Revert check on clientID. We will no longer require this to be a string (PR #1233)
34+
35+
## [8.3.0] - released 2021-06-03
36+
### Added
37+
- The server will now validate redirect uris according to rfc8252 (PR #1203)
38+
- Events emitted now include the refresh token and access token payloads (PR #1211)
39+
- Use the `revokeRefreshTokens()` function to decide whether refresh tokens are revoked or not upon use (PR #1189)
40+
41+
### Changed
42+
- Keys are now validated using `openssl_pkey_get_private()` and `openssl_pkey_get_public()` instead of regex matching (PR #1215)
43+
44+
### Fixed
45+
- 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)
46+
- Added type validation for redirect uri, client ID, client secret, scopes, auth code, state, username, and password inputs (PR #1210)
47+
- Allow scope "0" to be used. Previously this was removed from a request because it failed an `empty()` check (PR #1181)
48+
49+
## [8.2.4] - released 2020-12-10
50+
### Fixed
51+
- Reverted the enforcement of at least one redirect_uri for a client. This change has instead been moved to version 9 (PR #1169)
2052

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

535567
- First major release
536568

537-
[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.2.3...HEAD
569+
[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.3.3...HEAD
570+
[8.3.3]: https://github.com/thephpleague/oauth2-server/compare/8.3.2...8.3.3
571+
[8.3.2]: https://github.com/thephpleague/oauth2-server/compare/8.3.1...8.3.2
572+
[8.3.1]: https://github.com/thephpleague/oauth2-server/compare/8.3.0...8.3.1
573+
[8.3.0]: https://github.com/thephpleague/oauth2-server/compare/8.2.4...8.3.0
574+
[8.2.4]: https://github.com/thephpleague/oauth2-server/compare/8.2.3...8.2.4
538575
[8.2.3]: https://github.com/thephpleague/oauth2-server/compare/8.2.2...8.2.3
539576
[8.2.2]: https://github.com/thephpleague/oauth2-server/compare/8.2.1...8.2.2
540577
[8.2.1]: https://github.com/thephpleague/oauth2-server/compare/8.2.0...8.2.1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ We use [Github Actions](https://github.com/features/actions), [Scrutinizer](http
6868
* [Laravel Passport](https://github.com/laravel/passport)
6969
* [OAuth 2 Server for CakePHP 3](https://github.com/uafrica/oauth-server)
7070
* [OAuth 2 Server for Mezzio](https://github.com/mezzio/mezzio-authentication-oauth2)
71-
* [Trikoder OAuth 2 Bundle (Symfony)](https://github.com/trikoder/oauth2-bundle)
71+
* [OAuth 2 Server Bundle (Symfony)](https://github.com/thephpleague/oauth2-server-bundle)
7272
* [Heimdall for CodeIgniter 4](https://github.com/ezralazuardy/heimdall)
7373

7474
## Changelog

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"php": "^7.2 || ^8.0",
88
"ext-openssl": "*",
99
"league/event": "^2.2",
10-
"lcobucci/jwt": "^3.4 || ^4.0",
10+
"lcobucci/jwt": "^3.4.6 || ^4.0.4",
1111
"psr/http-message": "^1.0.1",
1212
"defuse/php-encryption": "^2.2.1",
1313
"ext-json": "*"

examples/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
},
55
"require-dev": {
66
"league/event": "^2.2",
7-
"lcobucci/jwt": "^3.4 || ^4.0",
7+
"lcobucci/jwt": "^3.4.6 || ^4.0.4",
88
"psr/http-message": "^1.0.1",
99
"defuse/php-encryption": "^2.2.1",
1010
"laminas/laminas-diactoros": "^2.5.0"

src/AuthorizationServer.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class AuthorizationServer implements EmitterAwareInterface
7979
*/
8080
private $defaultScope = '';
8181

82+
/**
83+
* @var bool
84+
*/
85+
private $revokeRefreshTokens = true;
86+
8287
/**
8388
* New server instance.
8489
*
@@ -136,6 +141,7 @@ public function enableGrantType(GrantTypeInterface $grantType, DateInterval $acc
136141
$grantType->setPrivateKey($this->privateKey);
137142
$grantType->setEmitter($this->getEmitter());
138143
$grantType->setEncryptionKey($this->encryptionKey);
144+
$grantType->revokeRefreshTokens($this->revokeRefreshTokens);
139145

140146
$this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType;
141147
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL;
@@ -235,4 +241,14 @@ public function setDefaultScope($defaultScope)
235241
{
236242
$this->defaultScope = $defaultScope;
237243
}
244+
245+
/**
246+
* Sets whether to revoke refresh tokens or not (for all grant types).
247+
*
248+
* @param bool $revokeRefreshTokens
249+
*/
250+
public function revokeRefreshTokens(bool $revokeRefreshTokens): void
251+
{
252+
$this->revokeRefreshTokens = $revokeRefreshTokens;
253+
}
238254
}

src/AuthorizationValidators/BearerTokenValidator.php

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@
1212
use DateTimeZone;
1313
use Lcobucci\Clock\SystemClock;
1414
use Lcobucci\JWT\Configuration;
15-
use Lcobucci\JWT\Encoding\CannotDecodeContent;
1615
use Lcobucci\JWT\Signer\Key\InMemory;
17-
use Lcobucci\JWT\Signer\Key\LocalFileReference;
1816
use Lcobucci\JWT\Signer\Rsa\Sha256;
19-
use Lcobucci\JWT\Token\InvalidTokenStructure;
20-
use Lcobucci\JWT\Token\UnsupportedHeaderFound;
2117
use Lcobucci\JWT\Validation\Constraint\SignedWith;
18+
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
2219
use Lcobucci\JWT\Validation\Constraint\ValidAt;
2320
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
2421
use League\OAuth2\Server\CryptKeyInterface;
@@ -77,8 +74,13 @@ private function initJwtConfiguration()
7774
);
7875

7976
$this->jwtConfiguration->setValidationConstraints(
80-
new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))),
81-
new SignedWith(new Sha256(), LocalFileReference::file($this->publicKey->getKeyPath()))
77+
\class_exists(StrictValidAt::class)
78+
? new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get())))
79+
: new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))),
80+
new SignedWith(
81+
new Sha256(),
82+
InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '')
83+
)
8284
);
8385
}
8486

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

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

9799
try {
98-
// Attempt to parse and validate the JWT
100+
// Attempt to parse the JWT
99101
$token = $this->jwtConfiguration->parser()->parse($jwt);
102+
} catch (\Lcobucci\JWT\Exception $exception) {
103+
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
104+
}
100105

106+
try {
107+
// Attempt to validate the JWT
101108
$constraints = $this->jwtConfiguration->validationConstraints();
102-
103-
try {
104-
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
105-
} catch (RequiredConstraintsViolated $exception) {
106-
throw OAuthServerException::accessDenied('Access token could not be verified');
107-
}
108-
} catch (CannotDecodeContent | InvalidTokenStructure | UnsupportedHeaderFound $exception) {
109-
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
109+
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
110+
} catch (RequiredConstraintsViolated $exception) {
111+
throw OAuthServerException::accessDenied('Access token could not be verified');
110112
}
111113

112114
$claims = $token->claims();

src/CryptKey.php

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@
1212
namespace League\OAuth2\Server;
1313

1414
use LogicException;
15-
use RuntimeException;
1615

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

22+
private const FILE_PREFIX = 'file://';
23+
24+
/**
25+
* @var string Key contents
26+
*/
27+
protected $keyContents;
28+
2229
/**
2330
* @var string
2431
*/
@@ -36,67 +43,77 @@ class CryptKey implements CryptKeyInterface
3643
*/
3744
public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true)
3845
{
39-
if ($rsaMatch = \preg_match(static::RSA_KEY_PATTERN, $keyPath)) {
40-
$keyPath = $this->saveKeyToFile($keyPath);
41-
} elseif ($rsaMatch === false) {
42-
throw new \RuntimeException(
43-
\sprintf('PCRE error [%d] encountered during key match attempt', \preg_last_error())
44-
);
45-
}
46+
$this->passPhrase = $passPhrase;
4647

47-
if (\strpos($keyPath, 'file://') !== 0) {
48-
$keyPath = 'file://' . $keyPath;
49-
}
48+
if (\strpos($keyPath, self::FILE_PREFIX) !== 0 && $this->isValidKey($keyPath, $this->passPhrase ?? '')) {
49+
$this->keyContents = $keyPath;
50+
$this->keyPath = '';
51+
// There's no file, so no need for permission check.
52+
$keyPermissionsCheck = false;
53+
} elseif (\is_file($keyPath)) {
54+
if (\strpos($keyPath, self::FILE_PREFIX) !== 0) {
55+
$keyPath = self::FILE_PREFIX . $keyPath;
56+
}
5057

51-
if (!\file_exists($keyPath) || !\is_readable($keyPath)) {
52-
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
58+
if (!\is_readable($keyPath)) {
59+
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
60+
}
61+
$this->keyContents = \file_get_contents($keyPath);
62+
$this->keyPath = $keyPath;
63+
if (!$this->isValidKey($this->keyContents, $this->passPhrase ?? '')) {
64+
throw new LogicException('Unable to read key from file ' . $keyPath);
65+
}
66+
} else {
67+
throw new LogicException('Unable to read key from file ' . $keyPath);
5368
}
5469

5570
if ($keyPermissionsCheck === true) {
5671
// Verify the permissions of the key
57-
$keyPathPerms = \decoct(\fileperms($keyPath) & 0777);
72+
$keyPathPerms = \decoct(\fileperms($this->keyPath) & 0777);
5873
if (\in_array($keyPathPerms, ['400', '440', '600', '640', '660'], true) === false) {
59-
\trigger_error(\sprintf(
60-
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
61-
$keyPath,
62-
$keyPathPerms
63-
), E_USER_NOTICE);
74+
\trigger_error(
75+
\sprintf(
76+
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
77+
$this->keyPath,
78+
$keyPathPerms
79+
),
80+
E_USER_NOTICE
81+
);
6482
}
6583
}
84+
}
6685

67-
$this->keyPath = $keyPath;
68-
$this->passPhrase = $passPhrase;
86+
/**
87+
* Get key contents
88+
*
89+
* @return string Key contents
90+
*/
91+
public function getKeyContents(): string
92+
{
93+
return $this->keyContents;
6994
}
7095

7196
/**
72-
* @param string $key
97+
* Validate key contents.
7398
*
74-
* @throws RuntimeException
99+
* @param string $contents
100+
* @param string $passPhrase
75101
*
76-
* @return string
102+
* @return bool
77103
*/
78-
private function saveKeyToFile($key)
104+
private function isValidKey($contents, $passPhrase)
79105
{
80-
$tmpDir = \sys_get_temp_dir();
81-
$keyPath = $tmpDir . '/' . \sha1($key) . '.key';
82-
83-
if (\file_exists($keyPath)) {
84-
return 'file://' . $keyPath;
85-
}
86-
87-
if (\file_put_contents($keyPath, $key) === false) {
88-
// @codeCoverageIgnoreStart
89-
throw new RuntimeException(\sprintf('Unable to write key file to temporary directory "%s"', $tmpDir));
90-
// @codeCoverageIgnoreEnd
91-
}
92-
93-
if (\chmod($keyPath, 0600) === false) {
94-
// @codeCoverageIgnoreStart
95-
throw new RuntimeException(\sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath));
96-
// @codeCoverageIgnoreEnd
106+
$pkey = \openssl_pkey_get_private($contents, $passPhrase) ?: \openssl_pkey_get_public($contents);
107+
if ($pkey === false) {
108+
return false;
97109
}
110+
$details = \openssl_pkey_get_details($pkey);
98111

99-
return 'file://' . $keyPath;
112+
return $details !== false && \in_array(
113+
$details['type'] ?? -1,
114+
[OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC],
115+
true
116+
);
100117
}
101118

102119
/**

0 commit comments

Comments
 (0)