Skip to content

Commit

Permalink
merge master into this branch
Browse files Browse the repository at this point in the history
  • Loading branch information
Sephster committed Nov 14, 2024
2 parents 5e69f11 + dd22d86 commit 66c2a3a
Show file tree
Hide file tree
Showing 20 changed files with 500 additions and 210 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
php-version: [8.1, 8.2, 8.3]
php-version: [8.1, 8.2, 8.3, 8.4]
composer-stability: [prefer-lowest, prefer-stable]
operating-system:
- ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.1, 8.2, 8.3]
php: [8.1, 8.2, 8.3, 8.4]
os: [ubuntu-22.04]
stability: [prefer-lowest, prefer-stable]

Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@ 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
- Support for PHP 8.4 (PR #1454)

### Fixed
- In the Auth Code grant, when requesting an access token with an invalid auth code, we now respond with an invalid_grant error instead of invalid_request (PR #1433)
- Fixed spec compliance issue where device access token request was mistakenly expecting to receive scopes in the request (PR #1412)
- Refresh tokens pre version 9 might have had user IDs set as ints which meant they were incorrectly rejected. We now cast these values to strings to allow old refresh tokens (PR #1436)

## [9.0.1] - released 2024-10-14
### Fixed
- Auto-generated event emitter is now persisted. Previously, a new emitter was generated every time (PR #1428)
- Fixed bug where you could not omit a redirect uri even if one had not been specified during the auth request (PR #1428)
- Fixed bug where "state" parameter wasn't present on `invalid_scope` error response and wasn't on fragment part of `access_denied` redirect URI on Implicit grant (PR #1298)
- Fixed bug where disabling refresh token revocation via `revokeRefreshTokens(false)` unintentionally disables issuing new refresh token (PR #1449)

## [9.0.0] - released 2024-05-13
### Added
Expand Down Expand Up @@ -650,7 +662,8 @@ Version 5 is a complete code rewrite.

- First major release

[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/9.0.0...HEAD
[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/9.0.1...HEAD
[9.0.1]: https://github.com/thephpleague/oauth2-server/compare/9.0.0...9.0.1
[9.0.0]: https://github.com/thephpleague/oauth2-server/compare/9.0.0-RC1...9.0.0
[9.0.0-RC1]: https://github.com/thephpleague/oauth2-server/compare/8.5.4...9.0.0-RC1
[8.5.4]: https://github.com/thephpleague/oauth2-server/compare/8.5.3...8.5.4
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"homepage": "https://oauth2.thephpleague.com/",
"license": "MIT",
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"ext-openssl": "*",
"league/event": "^3.0",
"league/uri": "^7.0",
Expand All @@ -16,9 +16,9 @@
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6.15",
"laminas/laminas-diactoros": "^3.3.0",
"phpstan/phpstan": "^1.10.55",
"phpunit/phpunit": "^9.6.21",
"laminas/laminas-diactoros": "^3.5",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-phpunit": "^1.3.15",
"roave/security-advisories": "dev-master",
"phpstan/extension-installer": "^1.3.1",
Expand Down
9 changes: 9 additions & 0 deletions examples/src/Repositories/DeviceCodeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface;
use OAuth2ServerExamples\Entities\ClientEntity;
use OAuth2ServerExamples\Entities\DeviceCodeEntity;
use OAuth2ServerExamples\Entities\ScopeEntity;

class DeviceCodeRepository implements DeviceCodeRepositoryInterface
{
Expand Down Expand Up @@ -49,6 +50,14 @@ public function getDeviceCodeEntityByDeviceCode($deviceCode): ?DeviceCodeEntityI
$deviceCodeEntity->setIdentifier($deviceCode);
$deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('now +1 hour'));
$deviceCodeEntity->setClient($clientEntity);
$deviceCodeEntity->setLastPolledAt(new DateTimeImmutable());

$scopes = [];
foreach ($scopes as $scope) {
$scopeEntity = new ScopeEntity();
$scopeEntity->setIdentifier($scope);
$deviceCodeEntity->addScope($scopeEntity);
}

// The user identifier should be set when the user authenticates on the
// OAuth server, along with whether they approved the request
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@
<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHint" />
<rule ref="SlevomatCodingStandard.Commenting.EmptyComment" />
<rule ref="SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion" />
<rule ref="SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue" />
</ruleset>
2 changes: 1 addition & 1 deletion src/EventEmitting/EmitterAwarePolyfill.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trait EmitterAwarePolyfill

public function getEmitter(): EventEmitter
{
return $this->emitter ?? new EventEmitter();
return $this->emitter ??= new EventEmitter();
}

public function setEmitter(EventEmitter $emitter): self
Expand Down
16 changes: 8 additions & 8 deletions src/Exception/OAuthServerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class OAuthServerException extends Exception
/**
* Throw a new exception.
*/
final public function __construct(string $message, int $code, private string $errorType, private int $httpStatusCode = 400, private ?string $hint = null, private ?string $redirectUri = null, Throwable $previous = null)
final public function __construct(string $message, int $code, private string $errorType, private int $httpStatusCode = 400, private ?string $hint = null, private ?string $redirectUri = null, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->payload = [
Expand Down Expand Up @@ -88,7 +88,7 @@ public static function unsupportedGrantType(): static
/**
* Invalid request error.
*/
public static function invalidRequest(string $parameter, ?string $hint = null, Throwable $previous = null): static
public static function invalidRequest(string $parameter, ?string $hint = null, ?Throwable $previous = null): static
{
$errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' .
'includes a parameter more than once, or is otherwise malformed.';
Expand Down Expand Up @@ -141,7 +141,7 @@ public static function invalidCredentials(): static
*
* @codeCoverageIgnore
*/
public static function serverError(string $hint, Throwable $previous = null): static
public static function serverError(string $hint, ?Throwable $previous = null): static
{
return new static(
'The authorization server encountered an unexpected condition which prevented it from fulfilling'
Expand All @@ -158,15 +158,15 @@ public static function serverError(string $hint, Throwable $previous = null): st
/**
* Invalid refresh token.
*/
public static function invalidRefreshToken(?string $hint = null, Throwable $previous = null): static
public static function invalidRefreshToken(?string $hint = null, ?Throwable $previous = null): static
{
return new static('The refresh token is invalid.', 8, 'invalid_grant', 400, $hint, null, $previous);
}

/**
* Access denied.
*/
public static function accessDenied(?string $hint = null, ?string $redirectUri = null, Throwable $previous = null): static
public static function accessDenied(?string $hint = null, ?string $redirectUri = null, ?Throwable $previous = null): static
{
return new static(
'The resource owner or authorization server denied the request.',
Expand Down Expand Up @@ -207,15 +207,15 @@ public function getErrorType(): string
*
* @return static
*/
public static function expiredToken(?string $hint = null, Throwable $previous = null): static
public static function expiredToken(?string $hint = null, ?Throwable $previous = null): static
{
$errorMessage = 'The `device_code` has expired and the device ' .
'authorization session has concluded.';

return new static($errorMessage, 11, 'expired_token', 400, $hint, null, $previous);
}

public static function authorizationPending(string $hint = '', Throwable $previous = null): static
public static function authorizationPending(string $hint = '', ?Throwable $previous = null): static
{
return new static(
'The authorization request is still pending as the end user ' .
Expand All @@ -236,7 +236,7 @@ public static function authorizationPending(string $hint = '', Throwable $previo
*
* @return static
*/
public static function slowDown(string $hint = '', Throwable $previous = null): static
public static function slowDown(string $hint = '', ?Throwable $previous = null): static
{
return new static(
'The authorization request is still pending and polling should ' .
Expand Down
11 changes: 11 additions & 0 deletions src/Grant/AbstractAuthorizeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace League\OAuth2\Server\Grant;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;

Expand All @@ -35,4 +36,14 @@ protected function createAuthorizationRequest(): AuthorizationRequestInterface
{
return new AuthorizationRequest();
}

/**
* Get the client redirect URI.
*/
protected function getClientRedirectUri(ClientEntityInterface $client): string
{
return is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
}
}
2 changes: 1 addition & 1 deletion src/Grant/AbstractGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ protected function validateRedirectUri(
*
* @return ScopeEntityInterface[]
*/
public function validateScopes(string|array|null $scopes, string $redirectUri = null): array
public function validateScopes(string|array|null $scopes, ?string $redirectUri = null): array
{
if ($scopes === null) {
$scopes = [];
Expand Down
31 changes: 11 additions & 20 deletions src/Grant/AuthCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,15 @@ private function validateAuthorizationCode(
throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
}

// The redirect URI is required in this request
// The redirect URI is required in this request if it was specified
// in the authorization request
$redirectUri = $this->getRequestParameter('redirect_uri', $request);
if ($authCodePayload->redirect_uri !== '' && $redirectUri === null) {
if ($authCodePayload->redirect_uri !== null && $redirectUri === null) {
throw OAuthServerException::invalidRequest('redirect_uri');
}

if ($authCodePayload->redirect_uri !== $redirectUri) {
// If a redirect URI has been provided ensure it matches the stored redirect URI
if ($redirectUri !== null && $authCodePayload->redirect_uri !== $redirectUri) {
throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
}
}
Expand Down Expand Up @@ -281,17 +283,16 @@ public function validateAuthorizationRequest(ServerRequestInterface $request): A
throw OAuthServerException::invalidClient($request);
}

$defaultClientRedirectUri = is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
$stateParameter = $this->getQueryStringParameter('state', $request);

$scopes = $this->validateScopes(
$this->getQueryStringParameter('scope', $request, $this->defaultScope),
$redirectUri ?? $defaultClientRedirectUri
$this->makeRedirectUri(
$redirectUri ?? $this->getClientRedirectUri($client),
$stateParameter !== null ? ['state' => $stateParameter] : []
)
);

$stateParameter = $this->getQueryStringParameter('state', $request);

$authorizationRequest = $this->createAuthorizationRequest();
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
Expand Down Expand Up @@ -355,7 +356,7 @@ public function completeAuthorizationRequest(AuthorizationRequestInterface $auth
}

$finalRedirectUri = $authorizationRequest->getRedirectUri()
?? $this->getClientRedirectUri($authorizationRequest);
?? $this->getClientRedirectUri($authorizationRequest->getClient());

// The user approved the client, redirect them back with an auth code
if ($authorizationRequest->isAuthorizationApproved() === true) {
Expand Down Expand Up @@ -409,14 +410,4 @@ public function completeAuthorizationRequest(AuthorizationRequestInterface $auth
)
);
}

/**
* Get the client redirect URI if not set in the request.
*/
private function getClientRedirectUri(AuthorizationRequestInterface $authorizationRequest): string
{
return is_array($authorizationRequest->getClient()->getRedirectUri())
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri();
}
}
3 changes: 1 addition & 2 deletions src/Grant/DeviceCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ public function respondToAccessTokenRequest(
): ResponseTypeInterface {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
$deviceCodeEntity = $this->validateDeviceCode($request, $client);

$deviceCodeEntity->setLastPolledAt(new DateTimeImmutable());
Expand All @@ -153,7 +152,7 @@ public function respondToAccessTokenRequest(
}

// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier());
$finalizedScopes = $this->scopeRepository->finalizeScopes($deviceCodeEntity->getScopes(), $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier());

// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUserIdentifier(), $finalizedScopes);
Expand Down
28 changes: 13 additions & 15 deletions src/Grant/ImplicitGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,24 +111,24 @@ public function validateAuthorizationRequest(ServerRequestInterface $request): A
if ($redirectUri !== null) {
$this->validateRedirectUri($redirectUri, $client, $request);
} elseif (
is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
|| $client->getRedirectUri() === ''
$client->getRedirectUri() === '' ||
(is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1)
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
} else {
$redirectUri = is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
}

$stateParameter = $this->getQueryStringParameter('state', $request);

$scopes = $this->validateScopes(
$this->getQueryStringParameter('scope', $request, $this->defaultScope),
$redirectUri
$this->makeRedirectUri(
$redirectUri ?? $this->getClientRedirectUri($client),
$stateParameter !== null ? ['state' => $stateParameter] : [],
$this->queryDelimiter
)
);

$stateParameter = $this->getQueryStringParameter('state', $request);

$authorizationRequest = $this->createAuthorizationRequest();
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
Expand All @@ -152,11 +152,8 @@ public function completeAuthorizationRequest(AuthorizationRequestInterface $auth
throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
}

$clientRegisteredRedirectUri = is_array($authorizationRequest->getClient()->getRedirectUri())
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri();

$finalRedirectUri = $authorizationRequest->getRedirectUri() ?? $clientRegisteredRedirectUri;
$finalRedirectUri = $authorizationRequest->getRedirectUri()
?? $this->getClientRedirectUri($authorizationRequest->getClient());

// The user approved the client, redirect them back with an access token
if ($authorizationRequest->isAuthorizationApproved() === true) {
Expand Down Expand Up @@ -199,7 +196,8 @@ public function completeAuthorizationRequest(AuthorizationRequestInterface $auth
$finalRedirectUri,
[
'state' => $authorizationRequest->getState(),
]
],
$this->queryDelimiter
)
);
}
Expand Down
16 changes: 9 additions & 7 deletions src/Grant/RefreshTokenGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,20 @@ public function respondToAccessTokenRequest(
}

// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes);
$userId = $oldRefreshToken['user_id'];
if (is_int($userId)) {
$userId = (string) $userId;
}
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $userId, $scopes);
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
$responseType->setAccessToken($accessToken);

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

if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
$responseType->setRefreshToken($refreshToken);
}
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
$responseType->setRefreshToken($refreshToken);
}

return $responseType;
Expand Down
Loading

0 comments on commit 66c2a3a

Please sign in to comment.