Skip to content
Snippets Groups Projects
Commit 3faa7dd5 authored by Reiter, Christoph's avatar Reiter, Christoph :snake:
Browse files

Switch to the OIDC discover protocol for the provider config

The goal is to support every OIDC server that implements the discover
protocol (Keycloak for example). This allows us to fetch all the required
information at runtime without the user having to keep the settings
in sync with the used server. The config and public keys are cached for
one hour.

While in theory this works with non-keycloak it isn't tested yet, and we
still need keycloak specific settings for the API docs auth because we only
support keycloak with our frontend web components which we inject into the
openapi docs.

Fixes #3
parent 6652d9ba
No related branches found
No related tags found
1 merge request!14Switch to the OIDC discover protocol for the provider config
Pipeline #57274 passed
......@@ -2,25 +2,25 @@
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Keycloak;
namespace Dbp\Relay\AuthBundle\Tests\Authenticator;
use Dbp\Relay\AuthBundle\Keycloak\KeycloakBearerUser;
use Dbp\Relay\AuthBundle\Authenticator\BearerUser;
use PHPUnit\Framework\TestCase;
class KeycloakBearerUserTest extends TestCase
class BearerUserTest extends TestCase
{
public function testRolesWithNoRealUser()
{
$user = new KeycloakBearerUser(null, ['foobar']);
$user = new BearerUser(null, ['foobar']);
$this->assertSame(['foobar'], $user->getRoles());
}
public function testGetUserIdentifier()
{
$user = new KeycloakBearerUser(null, ['foobar']);
$user = new BearerUser(null, ['foobar']);
$this->assertSame('', $user->getUserIdentifier());
$this->assertSame('', $user->getUsername());
$user = new KeycloakBearerUser('quux', ['foobar']);
$user = new BearerUser('quux', ['foobar']);
$this->assertSame('quux', $user->getUserIdentifier());
$this->assertSame('quux', $user->getUsername());
}
......
......@@ -2,11 +2,11 @@
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Keycloak;
namespace Dbp\Relay\AuthBundle\Tests\Authenticator;
use Dbp\Relay\AuthBundle\Keycloak\Keycloak;
use Dbp\Relay\AuthBundle\Keycloak\KeycloakLocalTokenValidator;
use Dbp\Relay\AuthBundle\Keycloak\TokenValidationException;
use Dbp\Relay\AuthBundle\Authenticator\LocalTokenValidator;
use Dbp\Relay\AuthBundle\Authenticator\TokenValidationException;
use Dbp\Relay\AuthBundle\OIDC\OIDProvider;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
......@@ -14,23 +14,18 @@ use Jose\Component\Core\JWK;
use Jose\Easy\Build;
use Jose\Easy\JWSBuilder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
class KeycloakLocalTokenValidatorTest extends TestCase
class LocalTokenValidatorTest extends TestCase
{
/* @var KeycloakLocalTokenValidator */
/* @var LocalTokenValidator */
private $tokenValidator;
/* @var Keycloak */
private $keycloak;
private $oid;
protected function setUp(): void
{
$keycloak = new Keycloak('https://auth.example.com/auth', 'tugraz');
$this->keycloak = $keycloak;
$cache = new ArrayAdapter();
$this->tokenValidator = new KeycloakLocalTokenValidator($keycloak, $cache, 0);
$this->oid = new OIDProvider();
$this->tokenValidator = new LocalTokenValidator($this->oid, 0);
$this->mockResponses([]);
}
......@@ -58,6 +53,16 @@ class KeycloakLocalTokenValidatorTest extends TestCase
return ['keys' => [$this->getJWK()->toPublic()->jsonSerialize()]];
}
private function getDiscoverResponse()
{
return [
'issuer' => 'https://nope/issuer',
'jwks_uri' => 'https://nope/certs',
'introspection_endpoint' => 'https://nope/introspect',
'introspection_endpoint_auth_signing_alg_values_supported' => ['RS256'],
];
}
private function getJWT(array $options = [])
{
$jwk = $this->getJWK();
......@@ -69,7 +74,7 @@ class KeycloakLocalTokenValidatorTest extends TestCase
->nbf($time)
->jti('0123456789')
->alg('RS256')
->iss($options['issuer'] ?? $this->keycloak->getBaseUrlWithRealm())
->iss($options['issuer'] ?? $this->oid->getProviderConfig()->getIssuer())
->aud('audience1')
->aud('audience2')
->sub('subject');
......@@ -81,13 +86,15 @@ class KeycloakLocalTokenValidatorTest extends TestCase
private function mockResponses(array $responses)
{
$stack = HandlerStack::create(new MockHandler($responses));
$this->tokenValidator->setClientHandler($stack);
$this->oid->setClientHandler($stack);
}
private function mockJWKResponse()
{
$jwks = $this->getPublicJWKs();
$discover = $this->getDiscoverResponse();
$this->mockResponses([
new Response(200, ['Content-Type' => 'application/json'], json_encode($discover)),
new Response(200, ['Content-Type' => 'application/json'], json_encode($jwks)),
]);
}
......@@ -97,21 +104,21 @@ class KeycloakLocalTokenValidatorTest extends TestCase
$this->mockJWKResponse();
$result = $this->tokenValidator->validate($this->getJWT());
$this->expectExceptionMessageMatches('/Bad audience/');
KeycloakLocalTokenValidator::checkAudience($result, 'foo');
LocalTokenValidator::checkAudience($result, 'foo');
}
public function testCheckAudienceGood()
{
$this->mockJWKResponse();
$result = $this->tokenValidator->validate($this->getJWT());
KeycloakLocalTokenValidator::checkAudience($result, 'audience2');
KeycloakLocalTokenValidator::checkAudience($result, 'audience1');
LocalTokenValidator::checkAudience($result, 'audience2');
LocalTokenValidator::checkAudience($result, 'audience1');
$this->assertTrue(true);
}
public function testLocalNoResponse()
{
$this->mockResponses([]);
$this->mockJWKResponse();
$this->expectException(TokenValidationException::class);
$this->tokenValidator->validate('foobar');
}
......
......@@ -4,24 +4,29 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Keycloak;
use Dbp\Relay\AuthBundle\Keycloak\Keycloak;
use Dbp\Relay\AuthBundle\Keycloak\KeycloakRemoteTokenValidator;
use Dbp\Relay\AuthBundle\Keycloak\TokenValidationException;
use Dbp\Relay\AuthBundle\Authenticator\RemoteTokenValidator;
use Dbp\Relay\AuthBundle\Authenticator\TokenValidationException;
use Dbp\Relay\AuthBundle\OIDC\OIDProvider;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
class KeycloakRemoteTokenValidatorTest extends TestCase
class RemoteTokenValidatorTest extends TestCase
{
/* @var KeycloakRemoteTokenValidator */
/* @var RemoteTokenValidator */
private $tokenValidator;
private $oid;
protected function setUp(): void
{
$keycloak = new Keycloak('https://auth.example.com/auth', 'tugraz', 'client', 'secret');
$this->tokenValidator = new KeycloakRemoteTokenValidator($keycloak);
$this->oid = new OIDProvider();
$this->oid->setConfig([
'remote_validation_id' => 'foo',
'remote_validation_secret' => 'bar',
]);
$this->tokenValidator = new RemoteTokenValidator($this->oid);
$this->mockResponses([]);
}
......@@ -33,7 +38,7 @@ class KeycloakRemoteTokenValidatorTest extends TestCase
private function mockResponses(array $responses)
{
$stack = HandlerStack::create(new MockHandler($responses));
$this->tokenValidator->setClientHandler($stack);
$this->oid->setClientHandler($stack);
}
public function testValidateOK()
......@@ -54,6 +59,12 @@ class KeycloakRemoteTokenValidatorTest extends TestCase
];
$this->mockResponses([
new Response(200, ['Content-Type' => 'application/json'], json_encode([
'issuer' => 'https://auth.example.com/auth/realms/tugraz',
'jwks_uri' => 'https://nope/certs',
'introspection_endpoint' => 'https://nope/introspect',
'introspection_endpoint_auth_signing_alg_values_supported' => ['RS256'],
])),
new Response(200, ['Content-Type' => 'application/json'], json_encode($result)),
]);
......
......@@ -2,26 +2,26 @@
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Keycloak;
namespace Dbp\Relay\AuthBundle\Tests\Authenticator;
use Dbp\Relay\AuthBundle\Service\KeycloakUserSession;
use Dbp\Relay\AuthBundle\Service\OIDCUserSession;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
class KeycloakUserSessionTest extends TestCase
class UserSessionTest extends TestCase
{
public function testIsServiceAccountToken()
{
$this->assertTrue(KeycloakUserSession::isServiceAccountToken(['scope' => 'foo bar']));
$this->assertFalse(KeycloakUserSession::isServiceAccountToken(['scope' => 'openid foo bar']));
$this->assertFalse(KeycloakUserSession::isServiceAccountToken(['scope' => 'openid']));
$this->assertFalse(KeycloakUserSession::isServiceAccountToken(['scope' => 'foo openid bar']));
$this->assertFalse(KeycloakUserSession::isServiceAccountToken(['scope' => 'foo bar openid']));
$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']));
}
public function testGetLoggingId()
{
$session = new KeycloakUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag());
$session->setSessionToken([]);
$this->assertSame('unknown-unknown', $session->getSessionLoggingId());
......@@ -31,7 +31,7 @@ class KeycloakUserSessionTest extends TestCase
public function testGetUserRoles()
{
$session = new KeycloakUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag());
$session->setSessionToken([]);
$this->assertSame([], $session->getUserRoles());
$session->setSessionToken(['scope' => 'foo bar quux-buz a_b']);
......@@ -42,7 +42,7 @@ class KeycloakUserSessionTest extends TestCase
public function testGetSessionCacheKey()
{
$session = new KeycloakUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag());
$session->setSessionToken(['scope' => 'foo']);
$old = $session->getSessionCacheKey();
$session->setSessionToken(['scope' => 'bar']);
......@@ -52,7 +52,7 @@ class KeycloakUserSessionTest extends TestCase
public function testGetSessionTTL()
{
$session = new KeycloakUserSession(new ParameterBag());
$session = new OIDCUserSession(new ParameterBag());
$session->setSessionToken([]);
$this->assertSame(-1, $session->getSessionTTL());
......
......@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests;
use Dbp\Relay\AuthBundle\Keycloak\KeycloakBearerUserProviderInterface;
use Dbp\Relay\AuthBundle\Authenticator\BearerUserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
class DummyUserProvider implements KeycloakBearerUserProviderInterface
class DummyUserProvider implements BearerUserProviderInterface
{
private $user;
private $token;
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Keycloak;
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Dbp\Relay\AuthBundle\Keycloak\KeycloakBearerAuthenticator;
use Dbp\Relay\AuthBundle\Keycloak\KeycloakBearerUser;
use Dbp\Relay\AuthBundle\Tests\DummyUserProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class KeycloakBearerAuthenticatorTest extends ApiTestCase
{
public function testAuthenticateNoHeader()
{
$user = new KeycloakBearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'nope');
$auth = new KeycloakBearerAuthenticator($provider);
$req = new Request();
$this->expectException(BadCredentialsException::class);
$auth->authenticate($req);
}
public function testSupports()
{
$user = new KeycloakBearerUser('foo', ['role']);
$provider = new DummyUserProvider($user, 'bar');
$auth = new KeycloakBearerAuthenticator($provider);
$this->assertFalse($auth->supports(new Request()));
$r = new Request();
$r->headers->set('Authorization', 'foobar');
$this->assertTrue($auth->supports($r));
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Keycloak;
use Dbp\Relay\AuthBundle\Keycloak\Keycloak;
use PHPUnit\Framework\TestCase;
class KeycloakTest extends TestCase
{
public function testURLs()
{
$provider = new Keycloak('http://foo.bar', 'somerealm');
$this->assertSame('http://foo.bar/realms/somerealm', $provider->getBaseUrlWithRealm());
$this->assertSame('http://foo.bar/realms/somerealm/protocol/openid-connect/auth', $provider->getBaseAuthorizationUrl());
$this->assertSame('http://foo.bar/realms/somerealm/protocol/openid-connect/token/introspect', $provider->getTokenIntrospectionUrl());
$this->assertSame('http://foo.bar/realms/somerealm/protocol/openid-connect/token', $provider->getBaseAccessTokenUrl([]));
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\OIDC;
use Dbp\Relay\AuthBundle\OIDC\OIDProviderConfig;
use PHPUnit\Framework\TestCase;
class OIDProviderConfigTest extends TestCase
{
public function testConfig()
{
$data = [
'issuer' => 'issuer',
'jwks_uri' => 'jwks_uri',
'introspection_endpoint_auth_signing_alg_values_supported' => ['RS256'],
'introspection_endpoint' => 'introspection_endpoint',
];
$oid = OIDProviderConfig::fromString(json_encode($data));
$oid->getIntrospectionEndpointSigningAlgorithms();
$oid->getIntrospectionEndpoint();
$oid->getJwksUri();
$this->assertSame('issuer', $oid->getIssuer());
$this->assertSame(['RS256'], $oid->getIntrospectionEndpointSigningAlgorithms());
$this->assertSame('introspection_endpoint', $oid->getIntrospectionEndpoint());
$this->assertSame('jwks_uri', $oid->getJwksUri());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment