17
17
use Jose \Component \Core \AlgorithmManager ;
18
18
use Jose \Component \Core \JWK ;
19
19
use Jose \Component \Core \JWKSet ;
20
+ use Jose \Component \Encryption \JWEDecrypter ;
21
+ use Jose \Component \Encryption \JWETokenSupport ;
22
+ use Jose \Component \Encryption \Serializer \CompactSerializer as JweCompactSerializer ;
23
+ use Jose \Component \Encryption \Serializer \JWESerializerManager ;
20
24
use Jose \Component \Signature \JWSTokenSupport ;
21
25
use Jose \Component \Signature \JWSVerifier ;
22
- use Jose \Component \Signature \Serializer \CompactSerializer ;
26
+ use Jose \Component \Signature \Serializer \CompactSerializer as JwsCompactSerializer ;
23
27
use Jose \Component \Signature \Serializer \JWSSerializerManager ;
24
28
use Psr \Clock \ClockInterface ;
25
29
use Psr \Log \LoggerInterface ;
37
41
final class OidcTokenHandler implements AccessTokenHandlerInterface
38
42
{
39
43
use OidcTrait;
44
+ private ?JWKSet $ decryptionKeyset = null ;
45
+ private ?AlgorithmManager $ decryptionAlgorithms = null ;
46
+ private bool $ enforceEncryption = false ;
40
47
41
48
public function __construct (
42
49
private Algorithm |AlgorithmManager $ signatureAlgorithm ,
43
- private JWK |JWKSet $ jwkset ,
50
+ private JWK |JWKSet $ signatureKeyset ,
44
51
private string $ audience ,
45
52
private array $ issuers ,
46
53
private string $ claim = 'sub ' ,
@@ -51,50 +58,29 @@ public function __construct(
51
58
trigger_deprecation ('symfony/security-http ' , '7.1 ' , 'First argument must be instance of %s, %s given. ' , AlgorithmManager::class, Algorithm::class);
52
59
$ this ->signatureAlgorithm = new AlgorithmManager ([$ signatureAlgorithm ]);
53
60
}
54
- if ($ jwkset instanceof JWK ) {
61
+ if ($ signatureKeyset instanceof JWK ) {
55
62
trigger_deprecation ('symfony/security-http ' , '7.1 ' , 'Second argument must be instance of %s, %s given. ' , JWKSet::class, JWK ::class);
56
- $ this ->jwkset = new JWKSet ([$ jwkset ]);
63
+ $ this ->signatureKeyset = new JWKSet ([$ signatureKeyset ]);
57
64
}
58
65
}
59
66
67
+ public function enabledJweSupport (JWKSet $ decryptionKeyset , AlgorithmManager $ decryptionAlgorithms , bool $ enforceEncryption ): void
68
+ {
69
+ $ this ->decryptionKeyset = $ decryptionKeyset ;
70
+ $ this ->decryptionAlgorithms = $ decryptionAlgorithms ;
71
+ $ this ->enforceEncryption = $ enforceEncryption ;
72
+ }
73
+
60
74
public function getUserBadgeFrom (string $ accessToken ): UserBadge
61
75
{
62
76
if (!class_exists (JWSVerifier::class) || !class_exists (Checker \HeaderCheckerManager::class)) {
63
77
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". ' );
64
78
}
65
79
66
80
try {
67
- // Decode the token
68
- $ jwsVerifier = new JWSVerifier ($ this ->signatureAlgorithm );
69
- $ serializerManager = new JWSSerializerManager ([new CompactSerializer ()]);
70
- $ jws = $ serializerManager ->unserialize ($ accessToken );
71
- $ claims = json_decode ($ jws ->getPayload (), true );
72
-
73
- // Verify the signature
74
- if (!$ jwsVerifier ->verifyWithKeySet ($ jws , $ this ->jwkset , 0 )) {
75
- throw new InvalidSignatureException ();
76
- }
77
-
78
- // Verify the headers
79
- $ headerCheckerManager = new Checker \HeaderCheckerManager ([
80
- new Checker \AlgorithmChecker ($ this ->signatureAlgorithm ->list ()),
81
- ], [
82
- new JWSTokenSupport (),
83
- ]);
84
- // if this check fails, an InvalidHeaderException is thrown
85
- $ headerCheckerManager ->check ($ jws , 0 );
86
-
87
- // Verify the claims
88
- $ checkers = [
89
- new Checker \IssuedAtChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: false ),
90
- new Checker \NotBeforeChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: false ),
91
- new Checker \ExpirationTimeChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: false ),
92
- new Checker \AudienceChecker ($ this ->audience ),
93
- new Checker \IssuerChecker ($ this ->issuers ),
94
- ];
95
- $ claimCheckerManager = new ClaimCheckerManager ($ checkers );
96
- // if this check fails, an InvalidClaimException is thrown
97
- $ claimCheckerManager ->check ($ claims );
81
+ $ accessToken = $ this ->decryptIfNeeded ($ accessToken );
82
+ $ claims = $ this ->loadAndVerifyJws ($ accessToken );
83
+ $ this ->verifyClaims ($ claims );
98
84
99
85
if (empty ($ claims [$ this ->claim ])) {
100
86
throw new MissingClaimException (\sprintf ('"%s" claim not found. ' , $ this ->claim ));
@@ -111,4 +97,92 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
111
97
throw new BadCredentialsException ('Invalid credentials. ' , $ e ->getCode (), $ e );
112
98
}
113
99
}
100
+
101
+ private function loadAndVerifyJws (string $ accessToken ): array
102
+ {
103
+ // Decode the token
104
+ $ jwsVerifier = new JWSVerifier ($ this ->signatureAlgorithm );
105
+ $ serializerManager = new JWSSerializerManager ([new JwsCompactSerializer ()]);
106
+ $ jws = $ serializerManager ->unserialize ($ accessToken );
107
+
108
+ // Verify the signature
109
+ if (!$ jwsVerifier ->verifyWithKeySet ($ jws , $ this ->signatureKeyset , 0 )) {
110
+ throw new InvalidSignatureException ();
111
+ }
112
+
113
+ $ headerCheckerManager = new Checker \HeaderCheckerManager ([
114
+ new Checker \AlgorithmChecker ($ this ->signatureAlgorithm ->list ()),
115
+ ], [
116
+ new JWSTokenSupport (),
117
+ ]);
118
+ // if this check fails, an InvalidHeaderException is thrown
119
+ $ headerCheckerManager ->check ($ jws , 0 );
120
+
121
+ return json_decode ($ jws ->getPayload (), true );
122
+ }
123
+
124
+ private function verifyClaims (array $ claims ): array
125
+ {
126
+ // Verify the claims
127
+ $ checkers = [
128
+ new Checker \IssuedAtChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: true ),
129
+ new Checker \NotBeforeChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: true ),
130
+ new Checker \ExpirationTimeChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: true ),
131
+ new Checker \AudienceChecker ($ this ->audience ),
132
+ new Checker \IssuerChecker ($ this ->issuers ),
133
+ ];
134
+ $ claimCheckerManager = new ClaimCheckerManager ($ checkers );
135
+
136
+ // if this check fails, an InvalidClaimException is thrown
137
+ return $ claimCheckerManager ->check ($ claims );
138
+ }
139
+
140
+ private function decryptIfNeeded (string $ accessToken ): string
141
+ {
142
+ if (null === $ this ->decryptionKeyset || null === $ this ->decryptionAlgorithms ) {
143
+ $ this ->logger ?->debug('The encrypted tokens (JWE) are not supported. Skipping. ' );
144
+
145
+ return $ accessToken ;
146
+ }
147
+
148
+ $ jweHeaderChecker = new Checker \HeaderCheckerManager (
149
+ [
150
+ new Checker \AlgorithmChecker ($ this ->decryptionAlgorithms ->list ()),
151
+ new Checker \CallableChecker ('enc ' , fn ($ value ) => \in_array ($ value , $ this ->decryptionAlgorithms ->list ())),
152
+ new Checker \CallableChecker ('cty ' , fn ($ value ) => 'JWT ' === $ value ),
153
+ new Checker \IssuedAtChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: true ),
154
+ new Checker \NotBeforeChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: true ),
155
+ new Checker \ExpirationTimeChecker (clock: $ this ->clock , allowedTimeDrift: 0 , protectedHeaderOnly: true ),
156
+ ],
157
+ [new JWETokenSupport ()]
158
+ );
159
+ $ jweDecrypter = new JWEDecrypter ($ this ->decryptionAlgorithms , null );
160
+ $ serializerManager = new JWESerializerManager ([new JweCompactSerializer ()]);
161
+ try {
162
+ $ jwe = $ serializerManager ->unserialize ($ accessToken );
163
+ $ jweHeaderChecker ->check ($ jwe , 0 );
164
+ $ result = $ jweDecrypter ->decryptUsingKeySet ($ jwe , $ this ->decryptionKeyset , 0 );
165
+ if (false === $ result ) {
166
+ throw new \RuntimeException ('The JWE could not be decrypted. ' );
167
+ }
168
+
169
+ $ payload = $ jwe ->getPayload ();
170
+ if (null === $ payload ) {
171
+ throw new \RuntimeException ('The JWE payload is empty. ' );
172
+ }
173
+
174
+ return $ payload ;
175
+ } catch (\InvalidArgumentException |\RuntimeException $ e ) {
176
+ if ($ this ->enforceEncryption ) {
177
+ $ this ->logger ?->error('An error occurred while decrypting the token. ' , [
178
+ 'error ' => $ e ->getMessage (),
179
+ 'trace ' => $ e ->getTraceAsString (),
180
+ ]);
181
+ throw new BadCredentialsException ('Encrypted token is required. ' , 0 , $ e );
182
+ }
183
+ $ this ->logger ?->debug('The token decryption failed. Skipping as not mandatory. ' );
184
+
185
+ return $ accessToken ;
186
+ }
187
+ }
114
188
}
0 commit comments