Skip to content
Commits on Source (7)
......@@ -12,10 +12,16 @@
},
"packageRules": [
{
"matchPackagePrefixes": [
"symfony/"
],
"matchPackagePrefixes": ["symfony/"],
"allowedVersions": "<6"
},
{
"matchPackagePrefixes": ["web-token/"],
"allowedVersions": "<3"
},
{
"matchPackageNames": ["friendsofphp/php-cs-fixer"],
"allowedVersions": "<3.5"
}
],
"js": {
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0ccb1170c71748bfc6f97cfac5c20fd9",
"content-hash": "bf1b5a6f9a4b40dd9a855a73345cf4f5",
"packages": [
{
"name": "api-platform/core",
......@@ -229,11 +229,11 @@
},
{
"name": "dbp/relay-core-bundle",
"version": "v0.1.48",
"version": "v0.1.51",
"source": {
"type": "git",
"url": "https://gitlab.tugraz.at/dbp/relay/dbp-relay-core-bundle",
"reference": "022cfbb04346c7dbc0b988c61246d9f3ffa53b33"
"reference": "7430fbcfbaf61e3b2fb273c81777688c5e930120"
},
"require": {
"api-platform/core": "^2.6.8 <2.7.0",
......@@ -296,7 +296,7 @@
"AGPL-3.0-or-later"
],
"description": "The core bundle of the Relay API gateway",
"time": "2022-10-27T08:18:30+00:00"
"time": "2022-11-08T14:20:43+00:00"
},
{
"name": "doctrine/annotations",
......@@ -8001,16 +8001,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.8.11",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "46e223dd68a620da18855c23046ddb00940b4014"
"reference": "a59c8b5bfd4a236f27efc8b5ce72c313c2b54b5f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014",
"reference": "46e223dd68a620da18855c23046ddb00940b4014",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a59c8b5bfd4a236f27efc8b5ce72c313c2b54b5f",
"reference": "a59c8b5bfd4a236f27efc8b5ce72c313c2b54b5f",
"shasum": ""
},
"require": {
......@@ -8040,7 +8040,7 @@
],
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.8.11"
"source": "https://github.com/phpstan/phpstan/tree/1.9.1"
},
"funding": [
{
......@@ -8056,7 +8056,7 @@
"type": "tidelift"
}
],
"time": "2022-10-24T15:45:13+00:00"
"time": "2022-11-04T13:35:59+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
......@@ -8112,22 +8112,22 @@
},
{
"name": "phpstan/phpstan-symfony",
"version": "1.2.14",
"version": "1.2.16",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "f7dd737329504115adaa987697a759a66dd2ee8a"
"reference": "d6ea16206b1b645ded5b43736d8ef5ae1168eb55"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f7dd737329504115adaa987697a759a66dd2ee8a",
"reference": "f7dd737329504115adaa987697a759a66dd2ee8a",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/d6ea16206b1b645ded5b43736d8ef5ae1168eb55",
"reference": "d6ea16206b1b645ded5b43736d8ef5ae1168eb55",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.8.2"
"phpstan/phpstan": "^1.9.1"
},
"conflict": {
"symfony/framework-bundle": "<3.0"
......@@ -8177,9 +8177,9 @@
"description": "Symfony Framework extensions and rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues",
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.14"
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.16"
},
"time": "2022-10-05T11:19:29+00:00"
"time": "2022-11-04T13:16:15+00:00"
},
{
"name": "phpunit/php-code-coverage",
......@@ -10242,16 +10242,16 @@
},
{
"name": "vimeo/psalm",
"version": "4.29.0",
"version": "4.30.0",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
"reference": "7ec5ffbd5f68ae03782d7fd33fff0c45a69f95b3"
"reference": "d0bc6e25d89f649e4f36a534f330f8bb4643dd69"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/7ec5ffbd5f68ae03782d7fd33fff0c45a69f95b3",
"reference": "7ec5ffbd5f68ae03782d7fd33fff0c45a69f95b3",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/d0bc6e25d89f649e4f36a534f330f8bb4643dd69",
"reference": "d0bc6e25d89f649e4f36a534f330f8bb4643dd69",
"shasum": ""
},
"require": {
......@@ -10344,9 +10344,9 @@
],
"support": {
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm/tree/4.29.0"
"source": "https://github.com/vimeo/psalm/tree/4.30.0"
},
"time": "2022-10-11T17:09:17+00:00"
"time": "2022-11-06T20:37:08+00:00"
},
{
"name": "webmozart/path-util",
......
......@@ -23,9 +23,15 @@ class BearerAuthenticator extends AbstractAuthenticator implements LoggerAwareIn
private $userProvider;
public function __construct(BearerUserProviderInterface $userProvider)
/**
* @var OIDCUserSessionProviderInterface
*/
private $userSession;
public function __construct(OIDCUserSessionProviderInterface $userSession, BearerUserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
$this->userSession = $userSession;
}
public function supports(Request $request): ?bool
......@@ -54,8 +60,11 @@ class BearerAuthenticator extends AbstractAuthenticator implements LoggerAwareIn
$user = $this->userProvider->loadUserByToken($token);
return new SelfValidatingPassport(new UserBadge($user->getUserIdentifier(), function ($token) use ($user) {
$passport = new SelfValidatingPassport(new UserBadge($user->getUserIdentifier(), function ($token) use ($user) {
return $user;
}));
$passport->setAttribute('relay_user_session_provider', $this->userSession);
return $passport;
}
}
......@@ -4,8 +4,9 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Authenticator;
use Dbp\Relay\AuthBundle\API\UserRolesInterface;
use Dbp\Relay\AuthBundle\Helpers\Tools;
use Dbp\Relay\AuthBundle\OIDC\OIDProvider;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
......@@ -18,12 +19,17 @@ class BearerUserProvider implements BearerUserProviderInterface, LoggerAwareInte
private $config;
private $userSession;
private $oidProvider;
/**
* @var UserRolesInterface
*/
private $userRoles;
public function __construct(UserSessionInterface $userSession, OIDProvider $oidProvider)
public function __construct(OIDCUserSessionProviderInterface $userSession, OIDProvider $oidProvider, UserRolesInterface $userRoles)
{
$this->userSession = $userSession;
$this->config = [];
$this->oidProvider = $oidProvider;
$this->userRoles = $userRoles;
}
public function setConfig(array $config)
......@@ -79,8 +85,9 @@ class BearerUserProvider implements BearerUserProviderInterface, LoggerAwareInte
{
$session = $this->userSession;
$session->setSessionToken($jwt);
$scopes = Tools::extractScopes($jwt);
$identifier = $session->getUserIdentifier();
$userRoles = $session->getUserRoles();
$userRoles = $this->userRoles->getRoles($identifier, $scopes);
return new BearerUser(
$identifier,
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Authenticator;
use Dbp\Relay\CoreBundle\API\UserSessionProviderInterface;
interface OIDCUserSessionProviderInterface extends UserSessionProviderInterface
{
public function setSessionToken(?array $jwt): void;
}
......@@ -17,4 +17,12 @@ class Tools
new MessageFormatter('[{method}] {uri}: CODE={code}, ERROR={error}, CACHE={res_header_X-Kevinrob-Cache}')
);
}
/**
* Extract scopes from a decoded JWT.
*/
public static function extractScopes(array $jwt): array
{
return preg_split('/\s+/', $jwt['scope'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
}
}
......@@ -7,10 +7,13 @@ services:
autowire: true
autoconfigure: true
Dbp\Relay\AuthBundle\Service\OIDCUserSession:
Dbp\Relay\AuthBundle\Service\OIDCUserSessionProvider:
autowire: true
autoconfigure: true
Dbp\Relay\AuthBundle\Authenticator\OIDCUserSessionProviderInterface:
'@Dbp\Relay\AuthBundle\Service\OIDCUserSessionProvider'
Dbp\Relay\AuthBundle\OIDC\OIDProvider:
autowire: true
autoconfigure: true
......@@ -21,9 +24,6 @@ services:
Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface:
'@Dbp\Relay\AuthBundle\Authenticator\BearerAuthenticator'
Dbp\Relay\CoreBundle\API\UserSessionInterface:
'@Dbp\Relay\AuthBundle\Service\OIDCUserSession'
Dbp\Relay\AuthBundle\Service\DefaultUserRoles:
autowire: true
autoconfigure: true
......
......@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Service;
use Dbp\Relay\AuthBundle\API\UserRolesInterface;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Dbp\Relay\AuthBundle\Authenticator\OIDCUserSessionProviderInterface;
use Dbp\Relay\AuthBundle\Helpers\Tools;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class OIDCUserSession implements UserSessionInterface
class OIDCUserSessionProvider implements OIDCUserSessionProviderInterface
{
/**
* @var ?array
......@@ -20,16 +20,10 @@ class OIDCUserSession implements UserSessionInterface
*/
private $parameters;
/**
* @var UserRolesInterface
*/
private $userRoles;
public function __construct(ParameterBagInterface $parameters, UserRolesInterface $userRoles)
public function __construct(ParameterBagInterface $parameters)
{
$this->jwt = null;
$this->parameters = $parameters;
$this->userRoles = $userRoles;
}
public function getUserIdentifier(): ?string
......@@ -41,31 +35,12 @@ class OIDCUserSession implements UserSessionInterface
return $this->jwt['username'] ?? null;
}
private static function getScopes($jwt): array
{
return preg_split('/\s+/', $jwt['scope'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
}
public function getUserRoles(): array
{
assert($this->jwt !== null);
$scopes = self::getScopes($this->jwt);
$userIdentifier = $this->getUserIdentifier();
return $this->userRoles->getRoles($userIdentifier, $scopes);
}
public function getUserScopes(): array
{
return self::getScopes($this->jwt);
}
/**
* Given a token returns if the token was generated through a client credential flow.
*/
public static function isServiceAccountToken(array $jwt): bool
{
$scopes = self::getScopes($jwt);
$scopes = Tools::extractScopes($jwt);
// XXX: This is the main difference I found compared to other flows, but that's a Keycloak
// implementation detail I guess.
......
......@@ -8,6 +8,7 @@ use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Dbp\Relay\AuthBundle\Authenticator\BearerAuthenticator;
use Dbp\Relay\AuthBundle\Authenticator\BearerUser;
use Dbp\Relay\AuthBundle\Tests\DummyUserProvider;
use Dbp\Relay\AuthBundle\Tests\DummyUserSessionProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
......@@ -20,7 +21,7 @@ class BearerAuthenticatorTest extends ApiTestCase
{
$user = new BearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'nope');
$auth = new BearerAuthenticator($provider);
$auth = new BearerAuthenticator(new DummyUserSessionProvider(), $provider);
$req = new Request();
$this->expectException(BadCredentialsException::class);
......@@ -31,7 +32,7 @@ class BearerAuthenticatorTest extends ApiTestCase
{
$user = new BearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'nope');
$auth = new BearerAuthenticator($provider);
$auth = new BearerAuthenticator(new DummyUserSessionProvider(), $provider);
$req = new Request();
$req->headers->set('Authorization', 'Bearer nope');
......@@ -45,7 +46,7 @@ class BearerAuthenticatorTest extends ApiTestCase
{
$user = new BearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'bar');
$auth = new BearerAuthenticator($provider);
$auth = new BearerAuthenticator(new DummyUserSessionProvider(), $provider);
$this->assertFalse($auth->supports(new Request()));
......@@ -58,7 +59,7 @@ class BearerAuthenticatorTest extends ApiTestCase
{
$user = new BearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'bar');
$auth = new BearerAuthenticator($provider);
$auth = new BearerAuthenticator(new DummyUserSessionProvider(), $provider);
$response = $auth->onAuthenticationSuccess(new Request(), new NullToken(), 'firewall');
$this->assertNull($response);
}
......@@ -67,7 +68,7 @@ class BearerAuthenticatorTest extends ApiTestCase
{
$user = new BearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'bar');
$auth = new BearerAuthenticator($provider);
$auth = new BearerAuthenticator(new DummyUserSessionProvider(), $provider);
$response = $auth->onAuthenticationFailure(new Request(), new AuthenticationException());
$this->assertSame(403, $response->getStatusCode());
$this->assertNotNull(json_decode($response->getContent()));
......
......@@ -7,7 +7,8 @@ namespace Dbp\Relay\AuthBundle\Tests\Authenticator;
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Dbp\Relay\AuthBundle\Authenticator\BearerUserProvider;
use Dbp\Relay\AuthBundle\OIDC\OIDProvider;
use Dbp\Relay\AuthBundle\Tests\DummyUserSession;
use Dbp\Relay\AuthBundle\Service\DefaultUserRoles;
use Dbp\Relay\AuthBundle\Tests\DummyUserSessionProvider;
use Psr\Log\NullLogger;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
......@@ -16,28 +17,28 @@ class BearerUserProviderTest extends ApiTestCase
public function testWithIdentifier()
{
$oid = new OIDProvider();
$udprov = new DummyUserSession('foo', ['role']);
$prov = new BearerUserProvider($udprov, $oid);
$udprov = new DummyUserSessionProvider('foo');
$prov = new BearerUserProvider($udprov, $oid, new DefaultUserRoles());
$user = $prov->loadUserByValidatedToken([]);
$this->assertSame('foo', $user->getUserIdentifier());
$this->assertSame(['role'], $user->getRoles());
$this->assertSame([], $user->getRoles());
}
public function testWithoutIdentifier()
{
$oid = new OIDProvider();
$udprov = new DummyUserSession(null, ['role']);
$prov = new BearerUserProvider($udprov, $oid);
$udprov = new DummyUserSessionProvider(null);
$prov = new BearerUserProvider($udprov, $oid, new DefaultUserRoles());
$user = $prov->loadUserByValidatedToken([]);
$this->assertSame('', $user->getUserIdentifier());
$this->assertSame(['role'], $user->getRoles());
$this->assertSame([], $user->getRoles());
}
public function testInvalidTokenLocal()
{
$oid = new OIDProvider();
$udprov = new DummyUserSession('foo', ['role']);
$prov = new BearerUserProvider($udprov, $oid);
$udprov = new DummyUserSessionProvider('foo');
$prov = new BearerUserProvider($udprov, $oid, new DefaultUserRoles());
$prov->setLogger(new NullLogger());
$prov->setConfig([
'remote_validation' => false,
......
......@@ -4,8 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Authenticator;
use Dbp\Relay\AuthBundle\Service\DefaultUserRoles;
use Dbp\Relay\AuthBundle\Service\OIDCUserSession;
use Dbp\Relay\AuthBundle\Service\OIDCUserSessionProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
......@@ -13,16 +12,16 @@ class UserSessionTest extends TestCase
{
public function testIsServiceAccountToken()
{
$this->assertTrue(OIDCUserSession::isServiceAccountToken(['scope' => 'foo bar']));
$this->assertFalse(OIDCUserSession::isServiceAccountToken(['scope' => 'openid foo bar']));
$this->assertFalse(OIDCUserSession::isServiceAccountToken(['scope' => 'openid']));
$this->assertFalse(OIDCUserSession::isServiceAccountToken(['scope' => 'foo openid bar']));
$this->assertFalse(OIDCUserSession::isServiceAccountToken(['scope' => 'foo bar openid']));
$this->assertTrue(OIDCUserSessionProvider::isServiceAccountToken(['scope' => 'foo bar']));
$this->assertFalse(OIDCUserSessionProvider::isServiceAccountToken(['scope' => 'openid foo bar']));
$this->assertFalse(OIDCUserSessionProvider::isServiceAccountToken(['scope' => 'openid']));
$this->assertFalse(OIDCUserSessionProvider::isServiceAccountToken(['scope' => 'foo openid bar']));
$this->assertFalse(OIDCUserSessionProvider::isServiceAccountToken(['scope' => 'foo bar openid']));
}
public function testGetLoggingId()
{
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session = new OIDCUserSessionProvider(new ParameterBag());
$session->setSessionToken([]);
$this->assertSame('unknown-unknown', $session->getSessionLoggingId());
......@@ -30,20 +29,9 @@ class UserSessionTest extends TestCase
$this->assertSame('clientA-abfa50', $session->getSessionLoggingId());
}
public function testGetUserRoles()
{
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]);
$this->assertSame([], $session->getUserRoles());
$session->setSessionToken(['scope' => 'foo bar quux-buz a_b']);
$this->assertSame(
['ROLE_SCOPE_FOO', 'ROLE_SCOPE_BAR', 'ROLE_SCOPE_QUUX-BUZ', 'ROLE_SCOPE_A_B'],
$session->getUserRoles());
}
public function testGetSessionCacheKey()
{
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session = new OIDCUserSessionProvider(new ParameterBag());
$session->setSessionToken(['scope' => 'foo']);
$old = $session->getSessionCacheKey();
$session->setSessionToken(['scope' => 'bar']);
......@@ -53,7 +41,7 @@ class UserSessionTest extends TestCase
public function testGetSessionTTL()
{
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session = new OIDCUserSessionProvider(new ParameterBag());
$session->setSessionToken([]);
$this->assertSame(-1, $session->getSessionTTL());
......
......@@ -4,23 +4,19 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Dbp\Relay\AuthBundle\Authenticator\OIDCUserSessionProviderInterface;
class DummyUserSession implements UserSessionInterface
class DummyUserSessionProvider implements OIDCUserSessionProviderInterface
{
private $jwt;
private $id;
private $roles;
public function __construct(?string $id = 'id', array $roles = [])
public function __construct(?string $id = 'id')
{
$this->id = $id;
$this->roles = $roles;
}
public function setSessionToken(?array $jwt): void
{
$this->jwt = $jwt;
}
public function getUserIdentifier(): ?string
......@@ -28,11 +24,6 @@ class DummyUserSession implements UserSessionInterface
return $this->id;
}
public function getUserRoles(): array
{
return $this->roles;
}
public function getSessionLoggingId(): ?string
{
return 'logging-id';
......