Complex classes like IdTokenBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use IdTokenBuilder, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
35 | class IdTokenBuilder |
||
36 | { |
||
37 | private $issuer; |
||
38 | private $client; |
||
39 | private $user; |
||
40 | private $userAccount; |
||
41 | private $redirectUri; |
||
42 | private $userinfo; |
||
43 | private $signatureKeys; |
||
44 | private $lifetime; |
||
45 | private $scope = null; |
||
46 | private $requestedClaims = []; |
||
47 | private $claimsLocales = null; |
||
48 | private $accessTokenId = null; |
||
49 | private $authorizationCodeId = null; |
||
50 | private $nonce = null; |
||
51 | private $withAuthenticationTime = false; |
||
52 | private $jwsBuilder = null; |
||
53 | private $signatureAlgorithm = null; |
||
54 | private $jweBuilder; |
||
55 | private $keyEncryptionAlgorithm = null; |
||
56 | private $contentEncryptionAlgorithm = null; |
||
57 | private $expiresAt = null; |
||
58 | private $jkuFactory = null; |
||
59 | private $authorizationCodeRepository = null; |
||
60 | |||
61 | public function __construct(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, User $user, UserAccount $userAccount, string $redirectUri, ?JKUFactory $jkuFactory, ?AuthorizationCodeRepository $authorizationCodeRepository) |
||
62 | { |
||
63 | $this->issuer = $issuer; |
||
64 | $this->userinfo = $userinfo; |
||
65 | $this->lifetime = $lifetime; |
||
66 | $this->client = $client; |
||
67 | $this->user = $user; |
||
68 | $this->userAccount = $userAccount; |
||
69 | $this->redirectUri = $redirectUri; |
||
70 | $this->jkuFactory = $jkuFactory; |
||
71 | $this->authorizationCodeRepository = $authorizationCodeRepository; |
||
72 | } |
||
73 | |||
74 | public function setAccessToken(AccessToken $accessToken): void |
||
75 | { |
||
76 | $this->accessTokenId = $accessToken->getTokenId(); |
||
77 | $this->expiresAt = $accessToken->getExpiresAt(); |
||
78 | $this->scope = $accessToken->getParameter()->has('scope') ? $accessToken->getParameter()->get('scope') : null; |
||
79 | |||
80 | if ($accessToken->getMetadata()->has('authorization_code_id') && null !== $this->authorizationCodeRepository) { |
||
81 | $authorizationCodeId = new AuthorizationCodeId($accessToken->getMetadata()->get('authorization_code_id')); |
||
82 | $authorizationCode = $this->authorizationCodeRepository->find($authorizationCodeId); |
||
83 | if (null === $authorizationCode) { |
||
84 | return; |
||
85 | } |
||
86 | $this->authorizationCodeId = $authorizationCodeId; |
||
87 | $queryParams = $authorizationCode->getQueryParams(); |
||
88 | foreach (['nonce' => 'nonce', 'claims_locales' => 'claimsLocales'] as $k => $v) { |
||
89 | if (\array_key_exists($k, $queryParams)) { |
||
90 | $this->$v = $queryParams[$k]; |
||
91 | } |
||
92 | } |
||
93 | $this->withAuthenticationTime = \array_key_exists('max_age', $authorizationCode->getQueryParams()); |
||
94 | } |
||
95 | } |
||
96 | |||
97 | public function withAccessTokenId(AccessTokenId $accessTokenId): void |
||
98 | { |
||
99 | $this->accessTokenId = $accessTokenId; |
||
100 | } |
||
101 | |||
102 | public function withAuthorizationCodeId(AuthorizationCodeId $authorizationCodeId): void |
||
103 | { |
||
104 | $this->authorizationCodeId = $authorizationCodeId; |
||
105 | } |
||
106 | |||
107 | public function withClaimsLocales(string $claimsLocales): void |
||
108 | { |
||
109 | $this->claimsLocales = $claimsLocales; |
||
110 | } |
||
111 | |||
112 | public function withAuthenticationTime(): void |
||
113 | { |
||
114 | $this->withAuthenticationTime = true; |
||
115 | } |
||
116 | |||
117 | public function withScope(string $scope): void |
||
118 | { |
||
119 | $this->scope = $scope; |
||
120 | } |
||
121 | |||
122 | public function withRequestedClaims(array $requestedClaims): void |
||
123 | { |
||
124 | $this->requestedClaims = $requestedClaims; |
||
125 | } |
||
126 | |||
127 | public function withNonce(string $nonce): void |
||
128 | { |
||
129 | $this->nonce = $nonce; |
||
130 | } |
||
131 | |||
132 | public function withExpirationAt(\DateTimeImmutable $expiresAt): void |
||
133 | { |
||
134 | $this->expiresAt = $expiresAt; |
||
135 | } |
||
136 | |||
137 | public function withoutAuthenticationTime(): void |
||
138 | { |
||
139 | $this->withAuthenticationTime = false; |
||
140 | } |
||
141 | |||
142 | public function withSignature(JWSBuilder $jwsBuilder, JWKSet $signatureKeys, string $signatureAlgorithm): void |
||
143 | { |
||
144 | if (!\in_array($signatureAlgorithm, $jwsBuilder->getSignatureAlgorithmManager()->list(), true)) { |
||
145 | throw new \InvalidArgumentException(\Safe\sprintf('Unsupported signature algorithm "%s". Please use one of the following one: %s', $signatureAlgorithm, \implode(', ', $jwsBuilder->getSignatureAlgorithmManager()->list()))); |
||
146 | } |
||
147 | if (0 === $signatureKeys->count()) { |
||
148 | throw new \InvalidArgumentException('The signature key set must contain at least one key.'); |
||
149 | } |
||
150 | $this->jwsBuilder = $jwsBuilder; |
||
151 | $this->signatureKeys = $signatureKeys; |
||
152 | $this->signatureAlgorithm = $signatureAlgorithm; |
||
153 | } |
||
154 | |||
155 | public function withEncryption(JWEBuilder $jweBuilder, string $keyEncryptionAlgorithm, string $contentEncryptionAlgorithm): void |
||
156 | { |
||
157 | if (!\in_array($keyEncryptionAlgorithm, $jweBuilder->getKeyEncryptionAlgorithmManager()->list(), true)) { |
||
158 | throw new \InvalidArgumentException(\Safe\sprintf('Unsupported key encryption algorithm "%s". Please use one of the following one: %s', $keyEncryptionAlgorithm, \implode(', ', $jweBuilder->getKeyEncryptionAlgorithmManager()->list()))); |
||
159 | } |
||
160 | if (!\in_array($contentEncryptionAlgorithm, $jweBuilder->getContentEncryptionAlgorithmManager()->list(), true)) { |
||
161 | throw new \InvalidArgumentException(\Safe\sprintf('Unsupported content encryption algorithm "%s". Please use one of the following one: %s', $contentEncryptionAlgorithm, \implode(', ', $jweBuilder->getContentEncryptionAlgorithmManager()->list()))); |
||
162 | } |
||
163 | $this->jweBuilder = $jweBuilder; |
||
164 | $this->keyEncryptionAlgorithm = $keyEncryptionAlgorithm; |
||
165 | $this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm; |
||
166 | } |
||
167 | |||
168 | public function build(): string |
||
169 | { |
||
170 | if (null === $this->scope) { |
||
171 | throw new \LogicException('It is mandatory to set the scope.'); |
||
172 | } |
||
173 | $data = $this->userinfo->getUserinfo($this->client, $this->user, $this->userAccount, $this->redirectUri, $this->requestedClaims, $this->scope, $this->claimsLocales); |
||
174 | //$data = $this->updateClaimsWithAmrAndAcrInfo($data, $this->userAccount); |
||
|
|||
175 | $data = $this->updateClaimsWithAuthenticationTime($data, $this->user, $this->requestedClaims); |
||
176 | $data = $this->updateClaimsWithNonce($data); |
||
177 | if (null !== $this->signatureAlgorithm) { |
||
178 | $data = $this->updateClaimsWithJwtClaims($data); |
||
179 | $data = $this->updateClaimsWithTokenHash($data); |
||
180 | $data = $this->updateClaimsAudience($data); |
||
181 | $result = $this->computeIdToken($data); |
||
182 | } else { |
||
183 | $result = \Safe\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); |
||
184 | } |
||
185 | |||
186 | if (null !== $this->keyEncryptionAlgorithm && null !== $this->contentEncryptionAlgorithm) { |
||
187 | $result = $this->tryToEncrypt($this->client, $result); |
||
188 | } |
||
189 | |||
190 | return $result; |
||
191 | } |
||
192 | |||
193 | private function updateClaimsWithJwtClaims(array $claims): array |
||
194 | { |
||
195 | if (null === $this->expiresAt) { |
||
196 | $this->expiresAt = (new \DateTimeImmutable())->setTimestamp(\time() + $this->lifetime); |
||
197 | } |
||
198 | $claims += [ |
||
199 | 'iat' => \time(), |
||
200 | 'nbf' => \time(), |
||
201 | 'exp' => $this->expiresAt->getTimestamp(), |
||
202 | 'jti' => Base64Url::encode(\random_bytes(16)), |
||
203 | 'iss' => $this->issuer, |
||
204 | ]; |
||
205 | |||
206 | return $claims; |
||
207 | } |
||
208 | |||
209 | private function updateClaimsWithAuthenticationTime(array $claims, User $user, array $requestedClaims): array |
||
210 | { |
||
211 | if ((true === $this->withAuthenticationTime || \array_key_exists('auth_time', $requestedClaims)) && null !== $user->getLastLoginAt()) { |
||
212 | $claims['auth_time'] = $user->getLastLoginAt(); |
||
213 | } |
||
214 | |||
215 | return $claims; |
||
216 | } |
||
217 | |||
218 | private function updateClaimsWithNonce(array $claims): array |
||
219 | { |
||
220 | if (null !== $this->nonce) { |
||
221 | $claims['nonce'] = $this->nonce; |
||
222 | } |
||
223 | |||
224 | return $claims; |
||
225 | } |
||
226 | |||
227 | private function updateClaimsAudience(array $claims): array |
||
228 | { |
||
229 | $claims['aud'] = [ |
||
230 | $this->client->getPublicId()->getValue(), |
||
231 | $this->issuer, |
||
232 | ]; |
||
233 | $claims['azp'] = $this->client->getPublicId()->getValue(); |
||
234 | |||
235 | return $claims; |
||
236 | } |
||
237 | |||
238 | private function computeIdToken(array $claims): string |
||
239 | { |
||
240 | $signatureKey = $this->getSignatureKey($this->signatureAlgorithm); |
||
241 | $header = $this->getHeaders($signatureKey, $this->signatureAlgorithm); |
||
242 | $jsonConverter = new StandardConverter(); |
||
243 | $claims = $jsonConverter->encode($claims); |
||
244 | $jws = $this->jwsBuilder |
||
245 | ->create() |
||
246 | ->withPayload($claims) |
||
247 | ->addSignature($signatureKey, $header) |
||
248 | ->build(); |
||
249 | $serializer = new JwsCompactSerializer($jsonConverter); |
||
250 | |||
251 | return $serializer->serialize($jws, 0); |
||
252 | } |
||
253 | |||
254 | private function tryToEncrypt(Client $client, string $jwt): string |
||
255 | { |
||
256 | $clientKeySet = $this->getClientKeySet($client); |
||
257 | $keyEncryptionAlgorithm = $this->jweBuilder->getKeyEncryptionAlgorithmManager()->get($this->keyEncryptionAlgorithm); |
||
258 | $encryptionKey = $clientKeySet->selectKey('enc', $keyEncryptionAlgorithm); |
||
259 | if (null === $encryptionKey) { |
||
260 | throw new \InvalidArgumentException('No encryption key available for the client.'); |
||
261 | } |
||
262 | $header = [ |
||
263 | 'typ' => 'JWT', |
||
264 | 'jti' => Base64Url::encode(\random_bytes(16)), |
||
265 | 'alg' => $this->keyEncryptionAlgorithm, |
||
266 | 'enc' => $this->contentEncryptionAlgorithm, |
||
267 | ]; |
||
268 | $jwe = $this->jweBuilder |
||
269 | ->create() |
||
270 | ->withPayload($jwt) |
||
271 | ->withSharedProtectedHeader($header) |
||
272 | ->addRecipient($encryptionKey) |
||
273 | ->build(); |
||
274 | $jsonConverter = new StandardConverter(); |
||
275 | $serializer = new JweCompactSerializer($jsonConverter); |
||
276 | |||
277 | return $serializer->serialize($jwe, 0); |
||
278 | } |
||
279 | |||
280 | private function getSignatureKey(string $signatureAlgorithm): JWK |
||
281 | { |
||
282 | $keys = $this->signatureKeys; |
||
283 | if ($this->client->has('client_secret')) { |
||
284 | $jwk = JWK::create([ |
||
285 | 'kty' => 'oct', |
||
286 | 'use' => 'sig', |
||
287 | 'k' => Base64Url::encode($this->client->get('client_secret')), |
||
288 | ]); |
||
289 | $keys = $keys->with($jwk); |
||
290 | } |
||
291 | $signatureAlgorithm = $this->jwsBuilder->getSignatureAlgorithmManager()->get($signatureAlgorithm); |
||
292 | if ('none' === $signatureAlgorithm->name()) { |
||
293 | return JWK::create(['kty' => 'none', 'alg' => 'none', 'use' => 'sig']); |
||
294 | } |
||
295 | $signatureKey = $keys->selectKey('sig', $signatureAlgorithm); |
||
296 | if (null === $signatureKey) { |
||
297 | throw new \InvalidArgumentException('Unable to find a key to sign the ID Token. Please verify the selected key set contains suitable keys.'); |
||
298 | } |
||
299 | |||
300 | return $signatureKey; |
||
301 | } |
||
302 | |||
303 | private function getHeaders(JWK $signatureKey, string $signatureAlgorithm): array |
||
304 | { |
||
305 | $header = [ |
||
306 | 'typ' => 'JWT', |
||
307 | 'alg' => $signatureAlgorithm, |
||
308 | ]; |
||
309 | if ($signatureKey->has('kid')) { |
||
310 | $header['kid'] = $signatureKey->get('kid'); |
||
311 | } |
||
312 | |||
313 | return $header; |
||
314 | } |
||
315 | |||
316 | private function updateClaimsWithTokenHash(array $claims): array |
||
317 | { |
||
318 | if ('none' === $this->signatureAlgorithm) { |
||
319 | return $claims; |
||
320 | } |
||
321 | if (null !== $this->accessTokenId) { |
||
322 | $claims['at_hash'] = $this->getHash($this->accessTokenId); |
||
323 | } |
||
324 | if (null !== $this->authorizationCodeId) { |
||
325 | $claims['c_hash'] = $this->getHash($this->authorizationCodeId); |
||
326 | } |
||
327 | |||
328 | return $claims; |
||
329 | } |
||
330 | |||
331 | private function getHash(TokenId $tokenId): string |
||
332 | { |
||
333 | return Base64Url::encode(\mb_substr(\hash($this->getHashMethod(), $tokenId->getValue(), true), 0, $this->getHashSize(), '8bit')); |
||
334 | } |
||
335 | |||
336 | private function getHashMethod(): string |
||
359 | |||
360 | private function getHashSize(): int |
||
361 | { |
||
362 | $map = [ |
||
363 | 'HS256' => 16, |
||
364 | 'ES256' => 16, |
||
365 | 'RS256' => 16, |
||
366 | 'PS256' => 16, |
||
367 | 'HS384' => 24, |
||
368 | 'ES384' => 24, |
||
369 | 'RS384' => 24, |
||
370 | 'PS384' => 24, |
||
371 | 'HS512' => 32, |
||
372 | 'ES512' => 32, |
||
373 | 'RS512' => 32, |
||
374 | 'PS512' => 32, |
||
375 | ]; |
||
376 | |||
377 | if (!\array_key_exists($this->signatureAlgorithm, $map)) { |
||
378 | throw new \InvalidArgumentException(\Safe\sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm)); |
||
379 | } |
||
380 | |||
381 | return $map[$this->signatureAlgorithm]; |
||
382 | } |
||
383 | |||
384 | private function getClientKeySet(Client $client): JWKSet |
||
414 | } |
||
415 |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.