Skip to content

Commit f7dd62b

Browse files
committed
Merge remote-tracking branch 'upstream/master' into bugfix/scope-named-0-considered-to-be-invalid
2 parents 2f62832 + 0d57b70 commit f7dd62b

23 files changed

+643
-80
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ 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
9+
- The server will now validate redirect uris according to rfc8252 (PR #1203)
10+
- Events emitted now include the refresh token and access token payloads (PR #1211)
11+
12+
### Changed
13+
- Keys are now validated using `openssl_pkey_get_private()` and openssl_pkey_get_public()` instead of regex matching (PR #1215)
14+
815
### Fixed
916
- 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)
17+
- Added type validation for redirect uri, client ID, client secret, scopes, auth code, state, username, and password inputs (PR #1210)
1018

1119
## [8.2.4] - released 2020-12-10
1220
### Fixed

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 || ~4.0.0",
1111
"psr/http-message": "^1.0.1",
1212
"defuse/php-encryption": "^2.2.1",
1313
"ext-json": "*"

src/AuthorizationValidators/BearerTokenValidator.php

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@
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;
1716
use Lcobucci\JWT\Signer\Key\LocalFileReference;
1817
use Lcobucci\JWT\Signer\Rsa\Sha256;
19-
use Lcobucci\JWT\Token\InvalidTokenStructure;
20-
use Lcobucci\JWT\Token\UnsupportedHeaderFound;
2118
use Lcobucci\JWT\Validation\Constraint\SignedWith;
2219
use Lcobucci\JWT\Validation\Constraint\ValidAt;
2320
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
@@ -95,18 +92,18 @@ public function validateAuthorization(ServerRequestInterface $request)
9592
$jwt = \trim((string) \preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
9693

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

101+
try {
102+
// Attempt to validate the JWT
101103
$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);
104+
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
105+
} catch (RequiredConstraintsViolated $exception) {
106+
throw OAuthServerException::accessDenied('Access token could not be verified');
110107
}
111108

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

src/CryptKey.php

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
class CryptKey
1818
{
19+
/** @deprecated left for backward compatibility check */
1920
const RSA_KEY_PATTERN =
2021
'/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----)\R.*(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)\R?$/s';
2122

23+
private const FILE_PREFIX = 'file://';
24+
2225
/**
2326
* @var string
2427
*/
@@ -36,36 +39,43 @@ class CryptKey
3639
*/
3740
public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true)
3841
{
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-
}
42+
$this->passPhrase = $passPhrase;
4643

47-
if (\strpos($keyPath, 'file://') !== 0) {
48-
$keyPath = 'file://' . $keyPath;
49-
}
44+
if (\is_file($keyPath)) {
45+
if (\strpos($keyPath, self::FILE_PREFIX) !== 0) {
46+
$keyPath = self::FILE_PREFIX . $keyPath;
47+
}
5048

51-
if (!\file_exists($keyPath) || !\is_readable($keyPath)) {
52-
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
49+
if (!\is_readable($keyPath)) {
50+
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
51+
}
52+
$isFileKey = true;
53+
$contents = \file_get_contents($keyPath);
54+
$this->keyPath = $keyPath;
55+
} else {
56+
$isFileKey = false;
57+
$contents = $keyPath;
58+
$this->keyPath = $this->saveKeyToFile($keyPath);
5359
}
5460

5561
if ($keyPermissionsCheck === true) {
5662
// Verify the permissions of the key
57-
$keyPathPerms = \decoct(\fileperms($keyPath) & 0777);
63+
$keyPathPerms = \decoct(\fileperms($this->keyPath) & 0777);
5864
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);
65+
\trigger_error(
66+
\sprintf(
67+
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
68+
$this->keyPath,
69+
$keyPathPerms
70+
),
71+
E_USER_NOTICE
72+
);
6473
}
6574
}
6675

67-
$this->keyPath = $keyPath;
68-
$this->passPhrase = $passPhrase;
76+
if (!$this->isValidKey($contents, $this->passPhrase ?? '')) {
77+
throw new LogicException('Unable to read key' . ($isFileKey ? " from file $keyPath" : ''));
78+
}
6979
}
7080

7181
/**
@@ -81,7 +91,7 @@ private function saveKeyToFile($key)
8191
$keyPath = $tmpDir . '/' . \sha1($key) . '.key';
8292

8393
if (\file_exists($keyPath)) {
84-
return 'file://' . $keyPath;
94+
return self::FILE_PREFIX . $keyPath;
8595
}
8696

8797
if (\file_put_contents($keyPath, $key) === false) {
@@ -96,7 +106,30 @@ private function saveKeyToFile($key)
96106
// @codeCoverageIgnoreEnd
97107
}
98108

99-
return 'file://' . $keyPath;
109+
return self::FILE_PREFIX . $keyPath;
110+
}
111+
112+
/**
113+
* Validate key contents.
114+
*
115+
* @param string $contents
116+
* @param string $passPhrase
117+
*
118+
* @return bool
119+
*/
120+
private function isValidKey($contents, $passPhrase)
121+
{
122+
$pkey = \openssl_pkey_get_private($contents, $passPhrase) ?: \openssl_pkey_get_public($contents);
123+
if ($pkey === false) {
124+
return false;
125+
}
126+
$details = \openssl_pkey_get_details($pkey);
127+
128+
return $details !== false && \in_array(
129+
$details['type'] ?? -1,
130+
[OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC],
131+
true
132+
);
100133
}
101134

102135
/**

src/Grant/AbstractGrant.php

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use League\OAuth2\Server\Entities\ScopeEntityInterface;
2525
use League\OAuth2\Server\Exception\OAuthServerException;
2626
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
27+
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
2728
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
2829
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
2930
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
@@ -191,6 +192,10 @@ protected function validateClient(ServerRequestInterface $request)
191192
$redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
192193

193194
if ($redirectUri !== null) {
195+
if (!\is_string($redirectUri)) {
196+
throw OAuthServerException::invalidRequest('redirect_uri');
197+
}
198+
194199
$this->validateRedirectUri($redirectUri, $client, $request);
195200
}
196201

@@ -238,12 +243,16 @@ protected function getClientCredentials(ServerRequestInterface $request)
238243

239244
$clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
240245

241-
if (\is_null($clientId)) {
246+
if (!\is_string($clientId)) {
242247
throw OAuthServerException::invalidRequest('client_id');
243248
}
244249

245250
$clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
246251

252+
if ($clientSecret !== null && !\is_string($clientSecret)) {
253+
throw OAuthServerException::invalidRequest('client_secret');
254+
}
255+
247256
return [$clientId, $clientSecret];
248257
}
249258

@@ -262,14 +271,8 @@ protected function validateRedirectUri(
262271
ClientEntityInterface $client,
263272
ServerRequestInterface $request
264273
) {
265-
if (\is_string($client->getRedirectUri())
266-
&& (\strcmp($client->getRedirectUri(), $redirectUri) !== 0)
267-
) {
268-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
269-
throw OAuthServerException::invalidClient($request);
270-
} elseif (\is_array($client->getRedirectUri())
271-
&& \in_array($redirectUri, $client->getRedirectUri(), true) === false
272-
) {
274+
$validator = new RedirectUriValidator($client->getRedirectUri());
275+
if (!$validator->validateRedirectUri($redirectUri)) {
273276
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
274277
throw OAuthServerException::invalidClient($request);
275278
}
@@ -287,10 +290,16 @@ protected function validateRedirectUri(
287290
*/
288291
public function validateScopes($scopes, $redirectUri = null)
289292
{
290-
if (!\is_array($scopes)) {
293+
if ($scopes === null) {
294+
$scopes = [];
295+
} elseif (\is_string($scopes)) {
291296
$scopes = $this->convertScopesQueryStringToArray($scopes);
292297
}
293298

299+
if (!\is_array($scopes)) {
300+
throw OAuthServerException::invalidRequest('scope');
301+
}
302+
294303
$validScopes = [];
295304

296305
foreach ($scopes as $scopeItem) {
@@ -313,7 +322,7 @@ public function validateScopes($scopes, $redirectUri = null)
313322
*
314323
* @return array
315324
*/
316-
private function convertScopesQueryStringToArray($scopes)
325+
private function convertScopesQueryStringToArray(string $scopes)
317326
{
318327
return \array_filter(\explode(self::SCOPE_DELIMITER_STRING, \trim($scopes)), function ($scope) {
319328
return $scope !== '';

src/Grant/AuthCodeGrant.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
use League\OAuth2\Server\Exception\OAuthServerException;
2121
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
2222
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
23+
use League\OAuth2\Server\RequestAccessTokenEvent;
2324
use League\OAuth2\Server\RequestEvent;
25+
use League\OAuth2\Server\RequestRefreshTokenEvent;
2426
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
2527
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
2628
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
@@ -106,7 +108,7 @@ public function respondToAccessTokenRequest(
106108

107109
$encryptedAuthCode = $this->getRequestParameter('code', $request, null);
108110

109-
if ($encryptedAuthCode === null) {
111+
if (!\is_string($encryptedAuthCode)) {
110112
throw OAuthServerException::invalidRequest('code');
111113
}
112114

@@ -162,14 +164,14 @@ public function respondToAccessTokenRequest(
162164

163165
// Issue and persist new access token
164166
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
165-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
167+
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
166168
$responseType->setAccessToken($accessToken);
167169

168170
// Issue and persist new refresh token if given
169171
$refreshToken = $this->issueRefreshToken($accessToken);
170172

171173
if ($refreshToken !== null) {
172-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
174+
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
173175
$responseType->setRefreshToken($refreshToken);
174176
}
175177

@@ -260,6 +262,10 @@ public function validateAuthorizationRequest(ServerRequestInterface $request)
260262
$redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
261263

262264
if ($redirectUri !== null) {
265+
if (!\is_string($redirectUri)) {
266+
throw OAuthServerException::invalidRequest('redirect_uri');
267+
}
268+
263269
$this->validateRedirectUri($redirectUri, $client, $request);
264270
} elseif (empty($client->getRedirectUri()) ||
265271
(\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {

src/Grant/ClientCredentialsGrant.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use DateInterval;
1515
use League\OAuth2\Server\Exception\OAuthServerException;
16+
use League\OAuth2\Server\RequestAccessTokenEvent;
1617
use League\OAuth2\Server\RequestEvent;
1718
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
1819
use Psr\Http\Message\ServerRequestInterface;
@@ -52,7 +53,7 @@ public function respondToAccessTokenRequest(
5253
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);
5354

5455
// Send event to emitter
55-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
56+
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
5657

5758
// Inject access token into response type
5859
$responseType->setAccessToken($accessToken);

0 commit comments

Comments
 (0)