34
34
use Symfony \Component \Security \Http \AccessToken \Oidc \Exception \MissingClaimException ;
35
35
use Symfony \Component \Security \Http \Authenticator \FallbackUserLoader ;
36
36
use Symfony \Component \Security \Http \Authenticator \Passport \Badge \UserBadge ;
37
+ use Symfony \Contracts \Cache \CacheInterface ;
38
+ use Symfony \Contracts \HttpClient \HttpClientInterface ;
37
39
38
40
/**
39
41
* The token handler decodes and validates the token, and retrieves the user identifier from it.
@@ -45,9 +47,14 @@ final class OidcTokenHandler implements AccessTokenHandlerInterface
45
47
private ?AlgorithmManager $ decryptionAlgorithms = null ;
46
48
private bool $ enforceEncryption = false ;
47
49
50
+ private ?CacheInterface $ discoveryCache = null ;
51
+ private ?HttpClientInterface $ discoveryClient = null ;
52
+ private ?string $ oidcConfigurationCacheKey = null ;
53
+ private ?string $ oidcJWKSetCacheKey = null ;
54
+
48
55
public function __construct (
49
56
private Algorithm |AlgorithmManager $ signatureAlgorithm ,
50
- private JWK |JWKSet $ signatureKeyset ,
57
+ private JWK |JWKSet | null $ signatureKeyset ,
51
58
private string $ audience ,
52
59
private array $ issuers ,
53
60
private string $ claim = 'sub ' ,
@@ -71,15 +78,64 @@ public function enabledJweSupport(JWKSet $decryptionKeyset, AlgorithmManager $de
71
78
$ this ->enforceEncryption = $ enforceEncryption ;
72
79
}
73
80
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
+
74
89
public function getUserBadgeFrom (string $ accessToken ): UserBadge
75
90
{
76
91
if (!class_exists (JWSVerifier::class) || !class_exists (Checker \HeaderCheckerManager::class)) {
77
92
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". ' );
78
93
}
79
94
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
+
80
136
try {
81
137
$ accessToken = $ this ->decryptIfNeeded ($ accessToken );
82
- $ claims = $ this ->loadAndVerifyJws ($ accessToken );
138
+ $ claims = $ this ->loadAndVerifyJws ($ accessToken, $ jwkset );
83
139
$ this ->verifyClaims ($ claims );
84
140
85
141
if (empty ($ claims [$ this ->claim ])) {
@@ -98,15 +154,15 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
98
154
}
99
155
}
100
156
101
- private function loadAndVerifyJws (string $ accessToken ): array
157
+ private function loadAndVerifyJws (string $ accessToken, JWKSet $ jwkset ): array
102
158
{
103
159
// Decode the token
104
160
$ jwsVerifier = new JWSVerifier ($ this ->signatureAlgorithm );
105
161
$ serializerManager = new JWSSerializerManager ([new JwsCompactSerializer ()]);
106
162
$ jws = $ serializerManager ->unserialize ($ accessToken );
107
163
108
164
// Verify the signature
109
- if (!$ jwsVerifier ->verifyWithKeySet ($ jws , $ this -> signatureKeyset , 0 )) {
165
+ if (!$ jwsVerifier ->verifyWithKeySet ($ jws , $ jwkset , 0 )) {
110
166
throw new InvalidSignatureException ();
111
167
}
112
168
0 commit comments