Skip to content

Commit

Permalink
Merge pull request #24 from trikoder/scope-inheritance
Browse files Browse the repository at this point in the history
Implement client scope inheritance and restriction
  • Loading branch information
spideyfusion authored Apr 24, 2019
2 parents 4973e1c + 50dce9f commit af9bffc
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 66 deletions.
4 changes: 4 additions & 0 deletions Converter/ScopeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public function toDomain(ScopeEntity $scope): ScopeModel
}

/**
* @param ScopeEntity[] $scopes
*
* @return ScopeModel[]
*/
public function toDomainArray(array $scopes): array
Expand All @@ -31,6 +33,8 @@ public function toLeague(ScopeModel $scope): ScopeEntity
}

/**
* @param ScopeModel[] $scopes
*
* @return ScopeEntity[]
*/
public function toLeagueArray(array $scopes): array
Expand Down
5 changes: 1 addition & 4 deletions Event/ScopeResolveEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,9 @@ public function getScopes(): array
return $this->scopes;
}

/**
* @param Scope[] $scopes
*/
public function setScopes(Scope ...$scopes): self
{
$this->scopes = (array) $scopes;
$this->scopes = $scopes;

return $this;
}
Expand Down
6 changes: 0 additions & 6 deletions Event/UserResolveEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,11 @@ public function getClient(): Client
return $this->client;
}

/**
* @return UserInterface
*/
public function getUser(): ?UserInterface
{
return $this->user;
}

/**
* @param UserInterface $user
*/
public function setUser(?UserInterface $user): self
{
$this->user = $user;
Expand Down
37 changes: 36 additions & 1 deletion League/Repository/ScopeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
namespace Trikoder\Bundle\OAuth2Bundle\League\Repository;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Trikoder\Bundle\OAuth2Bundle\Converter\ScopeConverter;
use Trikoder\Bundle\OAuth2Bundle\Event\ScopeResolveEvent;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\Client as ClientModel;
use Trikoder\Bundle\OAuth2Bundle\Model\Grant as GrantModel;
use Trikoder\Bundle\OAuth2Bundle\Model\Scope as ScopeModel;
use Trikoder\Bundle\OAuth2Bundle\OAuth2Events;

final class ScopeRepository implements ScopeRepositoryInterface
Expand Down Expand Up @@ -71,10 +74,12 @@ public function finalizeScopes(
) {
$client = $this->clientManager->find($clientEntity->getIdentifier());

$scopes = $this->setupScopes($client, $this->scopeConverter->toDomainArray($scopes));

$event = $this->eventDispatcher->dispatch(
OAuth2Events::SCOPE_RESOLVE,
new ScopeResolveEvent(
$this->scopeConverter->toDomainArray($scopes),
$scopes,
new GrantModel($grantType),
$client,
$userIdentifier
Expand All @@ -83,4 +88,34 @@ public function finalizeScopes(

return $this->scopeConverter->toLeagueArray($event->getScopes());
}

/**
* @param ScopeModel[] $requestedScopes
*
* @return ScopeModel[]
*/
private function setupScopes(ClientModel $client, array $requestedScopes): array
{
$clientScopes = $client->getScopes();

if (empty($clientScopes)) {
return $requestedScopes;
}

if (empty($requestedScopes)) {
return $clientScopes;
}

$finalizedScopes = [];

foreach ($requestedScopes as $requestedScope) {
if (!\in_array($requestedScope, $clientScopes, true)) {
throw OAuthServerException::invalidScope((string) $requestedScope);
}

$finalizedScopes[] = $requestedScope;
}

return $finalizedScopes;
}
}
6 changes: 6 additions & 0 deletions Manager/InMemory/AccessTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ final class AccessTokenManager implements AccessTokenManagerInterface
*/
private $accessTokens = [];

/**
* {@inheritdoc}
*/
public function find(string $identifier): ?AccessToken
{
return $this->accessTokens[$identifier] ?? null;
}

/**
* {@inheritdoc}
*/
public function save(AccessToken $accessToken): void
{
$this->accessTokens[$accessToken->getIdentifier()] = $accessToken;
Expand Down
4 changes: 2 additions & 2 deletions Model/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public function __construct(
DateTime $expiry,
Client $client,
?string $userIdentifier,
array $scopes)
{
array $scopes
) {
$this->identifier = $identifier;
$this->expiry = $expiry;
$this->client = $client;
Expand Down
15 changes: 3 additions & 12 deletions Model/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,9 @@ public function getRedirectUris(): array
return $this->redirectUris;
}

/**
* @param RedirectUri[] $redirectUris
*/
public function setRedirectUris(RedirectUri ...$redirectUris): self
{
$this->redirectUris = (array) $redirectUris;
$this->redirectUris = $redirectUris;

return $this;
}
Expand All @@ -81,12 +78,9 @@ public function getGrants(): array
return $this->grants;
}

/**
* @param Grant[] $grants
*/
public function setGrants(Grant ...$grants): self
{
$this->grants = (array) $grants;
$this->grants = $grants;

return $this;
}
Expand All @@ -99,12 +93,9 @@ public function getScopes(): array
return $this->scopes;
}

/**
* @param Scope[] $scopes
*/
public function setScopes(Scope ...$scopes): self
{
$this->scopes = (array) $scopes;
$this->scopes = $scopes;

return $this;
}
Expand Down
44 changes: 31 additions & 13 deletions Tests/Fixtures/FixtureFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken;
use Trikoder\Bundle\OAuth2Bundle\Model\Scope;

/**
* Development hints:
*
* You can easily generate token identifiers using the following command:
* --- dev/bin/php -r "echo bin2hex(random_bytes(40)) . PHP_EOL;"
*/
final class FixtureFactory
{
public const FIXTURE_ACCESS_TOKEN_USER_BOUND = '96fb0ff864bf242425bfa7b9b6f47294fda556bf5eef78f753f61c2b827125d37d5d5735bcaed5b8';
Expand All @@ -27,17 +33,27 @@ final class FixtureFactory
public const FIXTURE_REFRESH_TOKEN_DIFFERENT_CLIENT = '73b1618470fdccf1c96eda132f8a19d6da43c31e2efd19daeab2c98c0ac36bf95b3ea72fdc8d6752';
public const FIXTURE_REFRESH_TOKEN_EXPIRED = '3b3db453a137debb7b5f445c971bef18bb4f045d272a66a27054a0713096d2a8377679d204495c88';
public const FIXTURE_REFRESH_TOKEN_REVOKED = '63641841630c2e4d747e0f9ebe12ee04424e322874b8e68ef69fd58f1899ef70beb09733e23928a6';
public const FIXTURE_REFRESH_TOKEN_WITH_SCOPES = 'e47d593ed661840b3633e4577c3261ef57ba225be193b190deb69ee9afefdc19f54f890fbdda59f5';

public const FIXTURE_CLIENT_FIRST = 'foo';
public const FIXTURE_CLIENT_SECOND = 'bar';
public const FIXTURE_CLIENT_INACTIVE = 'baz_inactive';
public const FIXTURE_CLIENT_RESTRICTED_GRANTS = 'qux_restricted';
public const FIXTURE_CLIENT_RESTRICTED_GRANTS = 'qux_restricted_grants';
public const FIXTURE_CLIENT_RESTRICTED_SCOPES = 'quux_restricted_scopes';

public const FIXTURE_SCOPE_FIRST = 'fancy';
public const FIXTURE_SCOPE_SECOND = 'rock';

public const FIXTURE_USER = 'user';

public static function createUser(array $roles = []): User
{
$user = new User();
$user['roles'] = $roles;

return $user;
}

public static function initializeFixtures(
ScopeManagerInterface $scopeManager,
ClientManagerInterface $clientManager,
Expand All @@ -64,7 +80,7 @@ public static function initializeFixtures(
/**
* @return AccessToken[]
*/
public static function createAccessTokens(ScopeManagerInterface $scopeManager, ClientManagerInterface $clientManager): array
private static function createAccessTokens(ScopeManagerInterface $scopeManager, ClientManagerInterface $clientManager): array
{
$accessTokens = [];

Expand Down Expand Up @@ -131,7 +147,7 @@ public static function createAccessTokens(ScopeManagerInterface $scopeManager, C
/**
* @return RefreshToken[]
*/
public static function createRefreshTokens(AccessTokenManagerInterface $accessTokenManager): array
private static function createRefreshTokens(AccessTokenManagerInterface $accessTokenManager): array
{
$refreshTokens = [];

Expand Down Expand Up @@ -160,13 +176,19 @@ public static function createRefreshTokens(AccessTokenManagerInterface $accessTo
))
->revoke();

$refreshTokens[] = new RefreshToken(
self::FIXTURE_REFRESH_TOKEN_WITH_SCOPES,
new DateTime('+1 month'),
$accessTokenManager->find(self::FIXTURE_ACCESS_TOKEN_USER_BOUND_WITH_SCOPES)
);

return $refreshTokens;
}

/**
* @return Client[]
*/
public static function createClients(): array
private static function createClients(): array
{
$clients = [];

Expand All @@ -180,26 +202,22 @@ public static function createClients(): array
$clients[] = (new Client(self::FIXTURE_CLIENT_RESTRICTED_GRANTS, 'wicked'))
->setGrants(new Grant('password'));

$clients[] = (new Client(self::FIXTURE_CLIENT_RESTRICTED_SCOPES, 'beer'))
->setScopes(new Scope(self::FIXTURE_SCOPE_SECOND));

return $clients;
}

/**
* @return Scope[]
*/
public static function createScopes(): array
private static function createScopes(): array
{
$scopes = [];

$scopes[] = new Scope(self::FIXTURE_SCOPE_FIRST);
$scopes[] = new Scope(self::FIXTURE_SCOPE_SECOND);

return $scopes;
}

public static function createUser(array $roles = []): User
{
$user = new User();
$user['roles'] = $roles;

return $user;
}
}
66 changes: 65 additions & 1 deletion Tests/Integration/AuthorizationServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function testInactiveClient(): void

public function testRestrictedGrantClient(): void
{
$request = $this->createAuthorizationRequest('qux_restricted:wicked', [
$request = $this->createAuthorizationRequest('qux_restricted_grants:wicked', [
'grant_type' => 'client_credentials',
]);

Expand All @@ -103,6 +103,21 @@ public function testRestrictedGrantClient(): void
$this->assertSame('Client authentication failed', $response['message']);
}

public function testRestrictedScopeClient(): void
{
$request = $this->createAuthorizationRequest('quux_restricted_scopes:beer', [
'grant_type' => 'client_credentials',
'scope' => 'fancy rock',
]);

$response = $this->handleAuthorizationRequest($request);

// Response assertions.
$this->assertSame('invalid_scope', $response['error']);
$this->assertSame('The requested scope is invalid, unknown, or malformed', $response['message']);
$this->assertSame('Check the `fancy` scope', $response['hint']);
}

public function testInvalidGrantType(): void
{
$request = $this->createAuthorizationRequest('foo:secret', [
Expand Down Expand Up @@ -187,6 +202,37 @@ public function testValidClientCredentialsGrantWithScope(): void
);
}

public function testValidClientCredentialsGrantWithInheritedScope(): void
{
$request = $this->createAuthorizationRequest('quux_restricted_scopes:beer', [
'grant_type' => 'client_credentials',
]);

timecop_freeze(new DateTime());

$response = $this->handleAuthorizationRequest($request);

timecop_return();

$accessToken = $this->getAccessToken($response['access_token']);

// Response assertions.
$this->assertSame('Bearer', $response['token_type']);
$this->assertSame(3600, $response['expires_in']);
$this->assertInstanceOf(AccessToken::class, $accessToken);

// Make sure the access token is issued for the given client ID.
$this->assertSame('quux_restricted_scopes', $accessToken->getClient()->getIdentifier());

// The access token should have the requested scope.
$this->assertEquals(
[
$this->scopeManager->find(FixtureFactory::FIXTURE_SCOPE_SECOND),
],
$accessToken->getScopes()
);
}

public function testValidPasswordGrant(): void
{
$this->eventDispatcher->addListener('trikoder.oauth2.user_resolve', function (UserResolveEvent $event) {
Expand Down Expand Up @@ -321,6 +367,24 @@ public function testDifferentClientRefreshGrant(): void
$this->assertSame('Token is not linked to client', $response['hint']);
}

public function testDifferentScopeRefreshGrant(): void
{
$existingRefreshToken = $this->refreshTokenManager->find(FixtureFactory::FIXTURE_REFRESH_TOKEN_WITH_SCOPES);

$request = $this->createAuthorizationRequest('foo:secret', [
'grant_type' => 'refresh_token',
'scope' => 'rock',
'refresh_token' => TestHelper::generateEncryptedPayload($existingRefreshToken),
]);

$response = $this->handleAuthorizationRequest($request);

// Response assertions.
$this->assertSame('invalid_scope', $response['error']);
$this->assertSame('The requested scope is invalid, unknown, or malformed', $response['message']);
$this->assertSame('Check the `rock` scope', $response['hint']);
}

public function testExpiredRefreshGrant(): void
{
$existingRefreshToken = $this->refreshTokenManager->find(FixtureFactory::FIXTURE_REFRESH_TOKEN_EXPIRED);
Expand Down
Loading

0 comments on commit af9bffc

Please sign in to comment.