Skip to content

Commit e4e8601

Browse files
feat(security): OIDC discovery
1 parent 0af71b6 commit e4e8601

File tree

3 files changed

+91
-5
lines changed

3 files changed

+91
-5
lines changed

AccessToken/Oidc/OidcTokenHandler.php

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\MissingClaimException;
3535
use Symfony\Component\Security\Http\Authenticator\FallbackUserLoader;
3636
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
37+
use Symfony\Contracts\Cache\CacheInterface;
38+
use Symfony\Contracts\HttpClient\HttpClientInterface;
3739

3840
/**
3941
* The token handler decodes and validates the token, and retrieves the user identifier from it.
@@ -45,9 +47,14 @@ final class OidcTokenHandler implements AccessTokenHandlerInterface
4547
private ?AlgorithmManager $decryptionAlgorithms = null;
4648
private bool $enforceEncryption = false;
4749

50+
private ?CacheInterface $discoveryCache = null;
51+
private ?HttpClientInterface $discoveryClient = null;
52+
private ?string $oidcConfigurationCacheKey = null;
53+
private ?string $oidcJWKSetCacheKey = null;
54+
4855
public function __construct(
4956
private Algorithm|AlgorithmManager $signatureAlgorithm,
50-
private JWK|JWKSet $signatureKeyset,
57+
private JWK|JWKSet|null $signatureKeyset,
5158
private string $audience,
5259
private array $issuers,
5360
private string $claim = 'sub',
@@ -71,15 +78,64 @@ public function enabledJweSupport(JWKSet $decryptionKeyset, AlgorithmManager $de
7178
$this->enforceEncryption = $enforceEncryption;
7279
}
7380

81+
public function enabledDiscovery(CacheInterface $cache, HttpClientInterface $client, string $oidcConfigurationCacheKey, string $oidcJWKSetCacheKey): void
82+
{
83+
$this->discoveryCache = $cache;
84+
$this->discoveryClient = $client;
85+
$this->oidcConfigurationCacheKey = $oidcConfigurationCacheKey;
86+
$this->oidcJWKSetCacheKey = $oidcJWKSetCacheKey;
87+
}
88+
7489
public function getUserBadgeFrom(string $accessToken): UserBadge
7590
{
7691
if (!class_exists(JWSVerifier::class) || !class_exists(Checker\HeaderCheckerManager::class)) {
7792
throw new \LogicException('You cannot use the "oidc" 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".');
7893
}
7994

95+
if (!$this->discoveryCache && !$this->signatureKeyset) {
96+
throw new \LogicException('You cannot use the "oidc" token handler without JWKSet nor "discovery". Please configure JWKSet in the constructor, or call "enableDiscovery" method.');
97+
}
98+
99+
$jwkset = $this->signatureKeyset;
100+
if ($this->discoveryCache) {
101+
try {
102+
$oidcConfiguration = json_decode($this->discoveryCache->get($this->oidcConfigurationCacheKey, function (): string {
103+
$response = $this->discoveryClient->request('GET', '.well-known/openid-configuration');
104+
105+
return $response->getContent();
106+
}), true, 512, \JSON_THROW_ON_ERROR);
107+
} catch (\Throwable $e) {
108+
$this->logger?->error('An error occurred while requesting OIDC configuration.', [
109+
'error' => $e->getMessage(),
110+
'trace' => $e->getTraceAsString(),
111+
]);
112+
113+
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
114+
}
115+
116+
try {
117+
$jwkset = JWKSet::createFromJson(
118+
$this->discoveryCache->get($this->oidcJWKSetCacheKey, function () use ($oidcConfiguration): string {
119+
$response = $this->discoveryClient->request('GET', $oidcConfiguration['jwks_uri']);
120+
// we only need signature key
121+
$keys = array_filter($response->toArray()['keys'], static fn (array $key) => 'sig' === $key['use']);
122+
123+
return json_encode(['keys' => $keys]);
124+
})
125+
);
126+
} catch (\Throwable $e) {
127+
$this->logger?->error('An error occurred while requesting OIDC certs.', [
128+
'error' => $e->getMessage(),
129+
'trace' => $e->getTraceAsString(),
130+
]);
131+
132+
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
133+
}
134+
}
135+
80136
try {
81137
$accessToken = $this->decryptIfNeeded($accessToken);
82-
$claims = $this->loadAndVerifyJws($accessToken);
138+
$claims = $this->loadAndVerifyJws($accessToken, $jwkset);
83139
$this->verifyClaims($claims);
84140

85141
if (empty($claims[$this->claim])) {
@@ -98,15 +154,15 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
98154
}
99155
}
100156

101-
private function loadAndVerifyJws(string $accessToken): array
157+
private function loadAndVerifyJws(string $accessToken, JWKSet $jwkset): array
102158
{
103159
// Decode the token
104160
$jwsVerifier = new JWSVerifier($this->signatureAlgorithm);
105161
$serializerManager = new JWSSerializerManager([new JwsCompactSerializer()]);
106162
$jws = $serializerManager->unserialize($accessToken);
107163

108164
// Verify the signature
109-
if (!$jwsVerifier->verifyWithKeySet($jws, $this->signatureKeyset, 0)) {
165+
if (!$jwsVerifier->verifyWithKeySet($jws, $jwkset, 0)) {
110166
throw new InvalidSignatureException();
111167
}
112168

AccessToken/Oidc/OidcUserInfoTokenHandler.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\MissingClaimException;
1818
use Symfony\Component\Security\Http\Authenticator\FallbackUserLoader;
1919
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
20+
use Symfony\Contracts\Cache\CacheInterface;
2021
use Symfony\Contracts\HttpClient\HttpClientInterface;
2122

2223
/**
@@ -26,19 +27,47 @@ final class OidcUserInfoTokenHandler implements AccessTokenHandlerInterface
2627
{
2728
use OidcTrait;
2829

30+
private ?CacheInterface $discoveryCache = null;
31+
private ?string $oidcConfigurationCacheKey = null;
32+
2933
public function __construct(
3034
private HttpClientInterface $client,
3135
private ?LoggerInterface $logger = null,
3236
private string $claim = 'sub',
3337
) {
3438
}
3539

40+
public function enabledDiscovery(CacheInterface $cache, string $oidcConfigurationCacheKey): void
41+
{
42+
$this->discoveryCache = $cache;
43+
$this->oidcConfigurationCacheKey = $oidcConfigurationCacheKey;
44+
}
45+
3646
public function getUserBadgeFrom(string $accessToken): UserBadge
3747
{
48+
if (null !== $this->discoveryCache) {
49+
try {
50+
// Call OIDC discovery to retrieve userinfo endpoint
51+
// OIDC configuration is stored in cache
52+
$oidcConfiguration = json_decode($this->discoveryCache->get($this->oidcConfigurationCacheKey, function (): string {
53+
$response = $this->client->request('GET', '.well-known/openid-configuration');
54+
55+
return $response->getContent();
56+
}), true, 512, \JSON_THROW_ON_ERROR);
57+
} catch (\Throwable $e) {
58+
$this->logger?->error('An error occurred while requesting OIDC configuration.', [
59+
'error' => $e->getMessage(),
60+
'trace' => $e->getTraceAsString(),
61+
]);
62+
63+
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
64+
}
65+
}
66+
3867
try {
3968
// Call the OIDC server to retrieve the user info
4069
// If the token is invalid or expired, the OIDC server will return an error
41-
$claims = $this->client->request('GET', '', [
70+
$claims = $this->client->request('GET', $this->discoveryCache ? $oidcConfiguration['userinfo_endpoint'] : '', [
4271
'auth_bearer' => $accessToken,
4372
])->toArray();
4473

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Support hashing the hashed password using crc32c when putting the user in the session
1111
* Add support for closures in `#[IsGranted]`
1212
* Add `OAuth2TokenHandler` with OAuth2 Token Introspection support for `AccessTokenAuthenticator`
13+
* Add discovery support to `OidcTokenHandler` and `OidcUserInfoTokenHandler`
1314

1415
7.2
1516
---

0 commit comments

Comments
 (0)