Skip to content
Commits on Source (1)
  • Reiter, Christoph's avatar
    Add an interface that allows customizing the user roles · 89d4166c
    Reiter, Christoph authored
    This adds UserRolesInterface which is used for converting the
    oauth2 scopes to symfony roles.
    
    The default interface implementation converts them to "ROLE_SCOPE_FOO".
    
    The interface also gets passed the user ID and can fetch roles from other
    places as well, like LDAP, or ignore the scopes etc.
    
    Fixes #4
    89d4166c
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\API;
interface UserRolesInterface
{
/**
* @param string[] $scopes
*
* @return string[]
*/
public function getRoles(?string $userIdentifier, array $scopes): array;
}
......@@ -23,3 +23,10 @@ services:
Dbp\Relay\CoreBundle\API\UserSessionInterface:
'@Dbp\Relay\AuthBundle\Service\OIDCUserSession'
Dbp\Relay\AuthBundle\Service\DefaultUserRoles:
autowire: true
autoconfigure: true
Dbp\Relay\AuthBundle\API\UserRolesInterface:
'@Dbp\Relay\AuthBundle\Service\DefaultUserRoles'
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Service;
use Dbp\Relay\AuthBundle\API\UserRolesInterface;
class DefaultUserRoles implements UserRolesInterface
{
/**
* The default implementation converts OAuth2 scopes to Symfony roles
* by prefixing them with "ROLE_SCOPE_" and converting to uppercase.
*/
public function getRoles(?string $userIdentifier, array $scopes): array
{
$roles = [];
foreach ($scopes as $scope) {
$roles[] = 'ROLE_SCOPE_'.mb_strtoupper($scope);
}
return $roles;
}
}
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Service;
use Dbp\Relay\AuthBundle\API\UserRolesInterface;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
......@@ -19,10 +20,16 @@ class OIDCUserSession implements UserSessionInterface
*/
private $parameters;
public function __construct(ParameterBagInterface $parameters)
/**
* @var UserRolesInterface
*/
private $userRoles;
public function __construct(ParameterBagInterface $parameters, UserRolesInterface $userRoles)
{
$this->jwt = null;
$this->parameters = $parameters;
$this->userRoles = $userRoles;
}
public function getUserIdentifier(): ?string
......@@ -36,21 +43,18 @@ 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();
$scopes = [];
if ($this->jwt['scope'] ?? '' !== '') {
$scopes = explode(' ', $this->jwt['scope']);
}
$roles = [];
foreach ($scopes as $scope) {
$roles[] = 'ROLE_SCOPE_'.mb_strtoupper($scope);
}
return $roles;
return $this->userRoles->getRoles($userIdentifier, $scopes);
}
/**
......@@ -58,13 +62,11 @@ class OIDCUserSession implements UserSessionInterface
*/
public static function isServiceAccountToken(array $jwt): bool
{
if (!array_key_exists('scope', $jwt)) {
throw new \RuntimeException('Token missing scope key');
}
$scope = $jwt['scope'];
$scopes = self::getScopes($jwt);
// XXX: This is the main difference I found compared to other flows, but that's a Keycloak
// implementation detail I guess.
$has_openid_scope = in_array('openid', explode(' ', $scope), true);
$has_openid_scope = in_array('openid', $scopes, true);
return !$has_openid_scope;
}
......
......@@ -4,6 +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 PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
......@@ -21,7 +22,7 @@ class UserSessionTest extends TestCase
public function testGetLoggingId()
{
$session = new OIDCUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]);
$this->assertSame('unknown-unknown', $session->getSessionLoggingId());
......@@ -31,7 +32,7 @@ class UserSessionTest extends TestCase
public function testGetUserRoles()
{
$session = new OIDCUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]);
$this->assertSame([], $session->getUserRoles());
$session->setSessionToken(['scope' => 'foo bar quux-buz a_b']);
......@@ -42,7 +43,7 @@ class UserSessionTest extends TestCase
public function testGetSessionCacheKey()
{
$session = new OIDCUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken(['scope' => 'foo']);
$old = $session->getSessionCacheKey();
$session->setSessionToken(['scope' => 'bar']);
......@@ -52,7 +53,7 @@ class UserSessionTest extends TestCase
public function testGetSessionTTL()
{
$session = new OIDCUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]);
$this->assertSame(-1, $session->getSessionTTL());
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests;
use Dbp\Relay\AuthBundle\Service\DefaultUserRoles;
use PHPUnit\Framework\TestCase;
class DefaultUserRolesTest extends TestCase
{
public function testGetRoles()
{
$userRoles = new DefaultUserRoles();
$roles = $userRoles->getRoles(null, ['foo-bar']);
$this->assertSame(['ROLE_SCOPE_FOO-BAR'], $roles);
}
}