Skip to content

Commit

Permalink
Merge branch 'master' into psr-event-dispatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
eugene-borovov authored Jun 1, 2021
2 parents 1f75b46 + 9bfb699 commit 56b2deb
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 52 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
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]
### 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)

### 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
Expand Down
77 changes: 55 additions & 22 deletions src/CryptKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

class CryptKey
{
/** @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
*/
Expand All @@ -36,36 +39,43 @@ class CryptKey
*/
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 (\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));
}
$isFileKey = true;
$contents = \file_get_contents($keyPath);
$this->keyPath = $keyPath;
} else {
$isFileKey = false;
$contents = $keyPath;
$this->keyPath = $this->saveKeyToFile($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;
if (!$this->isValidKey($contents, $this->passPhrase ?? '')) {
throw new LogicException('Unable to read key' . ($isFileKey ? " from file $keyPath" : ''));
}
}

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

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

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

return 'file://' . $keyPath;
return self::FILE_PREFIX . $keyPath;
}

/**
* Validate key contents.
*
* @param string $contents
* @param string $passPhrase
*
* @return bool
*/
private function isValidKey($contents, $passPhrase)
{
$pkey = \openssl_pkey_get_private($contents, $passPhrase) ?: \openssl_pkey_get_public($contents);
if ($pkey === false) {
return false;
}
$details = \openssl_pkey_get_details($pkey);

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

/**
Expand Down
13 changes: 4 additions & 9 deletions src/Grant/AbstractGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use League\OAuth2\Server\Events\EventDispatcherAwareTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
Expand Down Expand Up @@ -270,14 +271,8 @@ protected function validateRedirectUri(
ClientEntityInterface $client,
ServerRequestInterface $request
) {
if (\is_string($client->getRedirectUri())
&& (\strcmp($client->getRedirectUri(), $redirectUri) !== 0)
) {
$this->dispatchEvent(new ClientAuthenticationFailed($client->getIdentifier(), $request));
throw OAuthServerException::invalidClient($request);
} elseif (\is_array($client->getRedirectUri())
&& \in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$validator = new RedirectUriValidator($client->getRedirectUri());
if (!$validator->validateRedirectUri($redirectUri)) {
$this->dispatchEvent(new ClientAuthenticationFailed($client->getIdentifier(), $request));
throw OAuthServerException::invalidClient($request);
}
Expand Down Expand Up @@ -330,7 +325,7 @@ public function validateScopes($scopes, $redirectUri = null)
private function convertScopesQueryStringToArray(string $scopes)
{
return \array_filter(\explode(self::SCOPE_DELIMITER_STRING, \trim($scopes)), function ($scope) {
return !empty($scope);
return $scope !== '';
});
}

Expand Down
114 changes: 114 additions & 0 deletions src/RedirectUriValidators/RedirectUriValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
/**
* @author Sebastiano Degan <[email protected]>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/

namespace League\OAuth2\Server\RedirectUriValidators;

class RedirectUriValidator implements RedirectUriValidatorInterface
{
/**
* @var array
*/
private $allowedRedirectUris;

/**
* New validator instance for the given uri
*
* @param string|array $allowedRedirectUris
*/
public function __construct($allowedRedirectUri)
{
if (\is_string($allowedRedirectUri)) {
$this->allowedRedirectUris = [$allowedRedirectUri];
} elseif (\is_array($allowedRedirectUri)) {
$this->allowedRedirectUris = $allowedRedirectUri;
} else {
$this->allowedRedirectUris = [];
}
}

/**
* Validates the redirect uri.
*
* @param string $redirectUri
*
* @return bool Return true if valid, false otherwise
*/
public function validateRedirectUri($redirectUri)
{
if ($this->isLoopbackUri($redirectUri)) {
return $this->matchUriExcludingPort($redirectUri);
}

return $this->matchExactUri($redirectUri);
}

/**
* According to section 7.3 of rfc8252, loopback uris are:
* - "http://127.0.0.1:{port}/{path}" for IPv4
* - "http://[::1]:{port}/{path}" for IPv6
*
* @param string $redirectUri
*
* @return bool
*/
private function isLoopbackUri($redirectUri)
{
$parsedUrl = \parse_url($redirectUri);

return $parsedUrl['scheme'] === 'http'
&& (\in_array($parsedUrl['host'], ['127.0.0.1', '[::1]'], true));
}

/**
* Find an exact match among allowed uris
*
* @param string $redirectUri
*
* @return bool Return true if an exact match is found, false otherwise
*/
private function matchExactUri($redirectUri)
{
return \in_array($redirectUri, $this->allowedRedirectUris, true);
}

/**
* Find a match among allowed uris, allowing for different port numbers
*
* @param string $redirectUri
*
* @return bool Return true if a match is found, false otherwise
*/
private function matchUriExcludingPort($redirectUri)
{
$parsedUrl = $this->parseUrlAndRemovePort($redirectUri);

foreach ($this->allowedRedirectUris as $allowedRedirectUri) {
if ($parsedUrl === $this->parseUrlAndRemovePort($allowedRedirectUri)) {
return true;
}
}

return false;
}

/**
* Parse an url like \parse_url, excluding the port
*
* @param string $url
*
* @return array
*/
private function parseUrlAndRemovePort($url)
{
$parsedUrl = \parse_url($url);
unset($parsedUrl['port']);

return $parsedUrl;
}
}
22 changes: 22 additions & 0 deletions src/RedirectUriValidators/RedirectUriValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/**
* @author Sebastiano Degan <[email protected]>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/

namespace League\OAuth2\Server\RedirectUriValidators;

interface RedirectUriValidatorInterface
{
/**
* Validates the redirect uri.
*
* @param string $redirectUri
*
* @return bool Return true if valid, false otherwise
*/
public function validateRedirectUri($redirectUri);
}
4 changes: 2 additions & 2 deletions tests/AuthorizationValidators/BearerTokenValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ public function testBearerTokenValidatorAcceptsValidToken()

$request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $validJwt->toString()));

$response = $bearerTokenValidator->validateAuthorization($request);
$validRequest = $bearerTokenValidator->validateAuthorization($request);

$this->assertArrayHasKey('authorization', $response->getHeaders());
$this->assertArrayHasKey('authorization', $validRequest->getHeaders());
}

public function testBearerTokenValidatorRejectsExpiredToken()
Expand Down
4 changes: 2 additions & 2 deletions tests/Grant/AbstractGrantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -521,13 +521,13 @@ public function testValidateScopes()
{
$scope = new ScopeEntity();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->expects($this->exactly(3))->method('getScopeEntityByIdentifier')->willReturn($scope);

/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setScopeRepository($scopeRepositoryMock);

$this->assertEquals([$scope], $grantMock->validateScopes('basic '));
$this->assertEquals([$scope, $scope, $scope], $grantMock->validateScopes('basic test 0 '));
}

public function testValidateScopesBadScope()
Expand Down
Loading

0 comments on commit 56b2deb

Please sign in to comment.