Skip to content

Commit

Permalink
fix: review
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Mar 14, 2024
1 parent 0eaca8e commit 50e908a
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 31 deletions.
Expand Up @@ -4,13 +4,10 @@

namespace App\Security\Http\AccessToken\Oidc;

use Jose\Component\Checker;
use Jose\Component\Core\JWKSet;
use Jose\Component\Signature\JWSLoader;
use Jose\Component\Signature\JWSVerifier;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\MissingClaimException;
Expand All @@ -20,6 +17,7 @@
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* Completes {@see OidcTokenHandler} with OIDC Discovery and configuration stored in cache.
Expand All @@ -33,8 +31,7 @@ public function __construct(
private CacheInterface $cache,
#[Autowire('@jose.jws_loader.oidc')]
private JWSLoader $jwsLoader,
#[Autowire('%env(OIDC_SERVER_URL)%')]
private string $issuer,
private readonly HttpClientInterface $securityAuthorizationClient,
private string $claim = 'email',
private int $ttl = 3600,
private ?LoggerInterface $logger = null,
Expand All @@ -43,36 +40,45 @@ public function __construct(

public function getUserBadgeFrom(string $accessToken): UserBadge
{
if (!class_exists(JWSVerifier::class) || !class_exists(Checker\HeaderCheckerManager::class)) {
throw new \LogicException('You cannot use the "oidc_discovery" token handler since "web-token/jwt-signature" and "web-token/jwt-checker" are not installed. Try running "composer require web-token/jwt-signature web-token/jwt-checker".');
}
try {
$oidcConfiguration = json_decode($this->cache->get('oidc.configuration', function (ItemInterface $item): string {
$item->expiresAfter($this->ttl);
$response = $this->securityAuthorizationClient->request('GET', '.well-known/openid-configuration');

if (!class_exists(HttpClient::class)) {
throw new \LogicException('You cannot use the "oidc_discovery" token handler since "symfony/http-client" is not installed. Try running "composer require symfony/http-client".');
}
return $response->getContent();
}), true, 512, \JSON_THROW_ON_ERROR);
} catch (\Throwable $e) {
$this->logger?->error('An error occurred while requesting OIDC configuration.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

$oidcConfiguration = json_decode($this->cache->get('oidc.configuration', function (ItemInterface $item): string {
$item->expiresAfter($this->ttl);
$response = HttpClient::create()->request('GET', rtrim($this->issuer, '/') . '/.well-known/openid-configuration');
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}

return $response->getContent();
}), true, 512, \JSON_THROW_ON_ERROR);
try {
$keyset = JWKSet::createFromJson(
$this->cache->get('oidc.jwkSet', function (ItemInterface $item) use ($oidcConfiguration): string {
$item->expiresAfter($this->ttl);
$response = $this->securityAuthorizationClient->request('GET', $oidcConfiguration['jwks_uri']);
// we only need signature key
$keys = array_filter($response->toArray()['keys'], static fn (array $key) => 'sig' === $key['use']);

$keyset = JWKSet::createFromJson(
$this->cache->get('oidc.jwkSet', function (ItemInterface $item) use ($oidcConfiguration): string {
$item->expiresAfter($this->ttl);
$client = HttpClient::create();
$response = $client->request('GET', $oidcConfiguration['jwks_uri']);
// we only need signature key
$keys = array_filter($response->toArray()['keys'], static fn (array $key) => 'sig' === $key['use']);
return json_encode(['keys' => $keys]);
})
);
} catch (\Throwable $e) {
$this->logger?->error('An error occurred while requesting OIDC certs.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

return json_encode(['keys' => $keys]);
})
);
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}

try {
// Decode the token
$signature = 0;
$signature = null;
$jws = $this->jwsLoader->loadAndVerifyWithKeySet(
token: $accessToken,
keyset: $keyset,
Expand All @@ -86,7 +92,7 @@ public function getUserBadgeFrom(string $accessToken): UserBadge

// UserLoader argument can be overridden by a UserProvider on AccessTokenAuthenticator::authenticate
return new UserBadge($claims[$this->claim], new FallbackUserLoader(fn () => $this->createUser($claims)), $claims);
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->logger?->error('An error occurred while decoding and validating the token.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
Expand Down
12 changes: 11 additions & 1 deletion api/src/Security/Voter/OIDCPermissionVoter.php
Expand Up @@ -5,12 +5,14 @@
namespace App\Security\Voter;

use ApiPlatform\Metadata\IriConverterInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Http\AccessToken\AccessTokenExtractorInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
Expand All @@ -30,6 +32,7 @@ public function __construct(
private readonly RequestStack $requestStack,
#[Autowire('@security.access_token_extractor.header')]
private readonly AccessTokenExtractorInterface $accessTokenExtractor,
private ?LoggerInterface $logger = null,
) {
}

Expand Down Expand Up @@ -67,7 +70,14 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter
]);

return $response->toArray()['result'] ?? false;
} catch (ExceptionInterface) {
} catch (HttpExceptionInterface) {
return false;
} catch (ExceptionInterface $e) {
$this->logger?->error('An error occurred while checking the permissions on OIDC server.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

return false;
}
}
Expand Down
11 changes: 9 additions & 2 deletions api/src/Security/Voter/OIDCRoleVoter.php
Expand Up @@ -5,6 +5,7 @@
namespace App\Security\Voter;

use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
Expand Down Expand Up @@ -34,6 +35,7 @@ public function __construct(
private readonly AccessTokenExtractorInterface $accessTokenExtractor,
#[Autowire('@jose.jws_serializer.oidc')]
private readonly JWSSerializerManager $jwsSerializerManager,
private ?LoggerInterface $logger = null,
) {
}

Expand Down Expand Up @@ -63,10 +65,15 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter
]);

$roles = array_map(static fn (string $role): string => strtolower($role), $response->toArray()['realm_access']['roles'] ?? []);
} catch (HttpExceptionInterface) {
} catch (HttpExceptionInterface $e) {
// OIDC server said no!
return false;
} catch (ExceptionInterface) {
} catch (ExceptionInterface $e) {
$this->logger?->error('An error occurred while checking the roles on OIDC server.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

// OIDC server doesn't seem to answer: check roles in token (if present)
$jws = $this->jwsSerializerManager->unserialize($accessToken);
$claims = json_decode($jws->getPayload(), true);
Expand Down

0 comments on commit 50e908a

Please sign in to comment.