Total Complexity | 40 |
Total Lines | 276 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like CookieService 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.
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 CookieService, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
45 | class CookieService implements CookieServiceInterface |
||
46 | { |
||
47 | /** |
||
48 | * @var CookieHelperInterface |
||
49 | */ |
||
50 | private $cookieHelper; |
||
51 | /** |
||
52 | * @var InstitutionConfigurationService |
||
53 | */ |
||
54 | private $institutionConfigurationService; |
||
55 | /** |
||
56 | * @var LoggerInterface |
||
57 | */ |
||
58 | private $logger; |
||
59 | /** |
||
60 | * @var SecondFactorService |
||
61 | */ |
||
62 | private $secondFactorService; |
||
63 | |||
64 | /** |
||
65 | * @var SecondFactorTypeService |
||
66 | */ |
||
67 | private $secondFactorTypeService; |
||
68 | /** |
||
69 | * @var ExpirationHelperInterface |
||
70 | */ |
||
71 | private $expirationHelper; |
||
72 | |||
73 | public function __construct( |
||
87 | } |
||
88 | |||
89 | public function handleSsoOn2faCookieStorage( |
||
90 | ResponseContext $responseContext, |
||
91 | Request $request, |
||
92 | Response $httpResponse, |
||
93 | string $authenticationMode = 'sso' |
||
94 | ): Response { |
||
95 | // Check if this specific SP is configured to allow setting of an SSO on 2FA cookie (configured in MW config) |
||
96 | $remoteSp = $this->getRemoteSp($responseContext); |
||
97 | if (!$remoteSp->allowedToSetSsoCookieOn2fa()) { |
||
98 | $this->logger->notice( |
||
99 | sprintf( |
||
100 | 'Ignoring SSO on 2FA for SP: %s', |
||
101 | $remoteSp->getEntityId() |
||
102 | ) |
||
103 | ); |
||
104 | return $httpResponse; |
||
105 | } |
||
106 | $secondFactorId = $responseContext->getSelectedSecondFactor(); |
||
107 | |||
108 | // We can only set an SSO on 2FA cookie if a second factor authentication is being handled. |
||
109 | if ($secondFactorId) { |
||
110 | $secondFactor = $this->secondFactorService->findByUuid($secondFactorId); |
||
111 | if (!$secondFactor) { |
||
112 | throw new RuntimeException(sprintf('Second Factor token not found with ID: %s', $secondFactorId)); |
||
113 | } |
||
114 | // Test if the institution of the Identity this SF belongs to has SSO on 2FA enabled |
||
115 | $isEnabled = $this->institutionConfigurationService->ssoOn2faEnabled($secondFactor->institution); |
||
116 | $this->logger->notice( |
||
117 | sprintf( |
||
118 | 'SSO on 2FA is %senabled for %s', |
||
119 | $isEnabled ? '' : 'not ', |
||
120 | $secondFactor->institution |
||
121 | ) |
||
122 | ); |
||
123 | if ($isEnabled) { |
||
124 | // The cookie reader can return a NullCookie if the cookie is not present, was expired or was otherwise |
||
125 | // deemed invalid. See the CookieHelper::read implementation for details. |
||
126 | $ssoCookie = $this->read($request); |
||
127 | $identityId = $responseContext->getIdentityNameId(); |
||
128 | $loa = $secondFactor->getLoaLevel($this->secondFactorTypeService); |
||
129 | $isValid = $this->isCookieValid($ssoCookie, $loa, $identityId); |
||
130 | $isVerifiedBySsoOn2faCookie = $responseContext->isVerifiedBySsoOn2faCookie(); |
||
131 | // Did the LoA requirement change? If a higher LoA was requested, or did a new token authentication |
||
132 | // take place? In that case create a new SSO on 2FA cookie |
||
133 | if ($this->shouldAddCookie($ssoCookie, $isValid, $loa, $isVerifiedBySsoOn2faCookie)) { |
||
134 | $cookie = CookieValue::from($identityId, $secondFactor->secondFactorId, $loa); |
||
135 | $this->store($httpResponse, $cookie); |
||
136 | } |
||
137 | } |
||
138 | } |
||
139 | $responseContext->finalizeAuthentication(); |
||
140 | return $httpResponse; |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * Allow high cyclomatic complexity in favour of keeping this method readable |
||
145 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
146 | * @SuppressWarnings(PHPMD.NPathComplexity) |
||
147 | */ |
||
148 | public function shouldSkip2faAuthentication( |
||
149 | ResponseContext $responseContext, |
||
150 | float $requiredLoa, |
||
151 | string $identityNameId, |
||
152 | Collection $secondFactorCollection, |
||
153 | Request $request |
||
154 | ): bool { |
||
155 | if ($responseContext->isForceAuthn()) { |
||
156 | $this->logger->notice('Ignoring SSO on 2FA cookie when ForceAuthN is specified.'); |
||
157 | return false; |
||
158 | } |
||
159 | $remoteSp = $this->getRemoteSp($responseContext); |
||
160 | // Test if the SP allows SSO on 2FA to take place (configured in MW config) |
||
161 | if (!$remoteSp->allowSsoOn2fa()) { |
||
162 | $this->logger->notice( |
||
163 | sprintf( |
||
164 | 'Ignoring SSO on 2FA for SP: %s', |
||
165 | $remoteSp->getEntityId() |
||
166 | ) |
||
167 | ); |
||
168 | return false; |
||
169 | } |
||
170 | $ssoCookie = $this->read($request); |
||
171 | // Perform validation on the cookie and its contents |
||
172 | if (!$this->isCookieValid($ssoCookie, $requiredLoa, $identityNameId)) { |
||
173 | return false; |
||
174 | } |
||
175 | if (!$this->secondFactorService->findByUuid($ssoCookie->secondFactorId())) { |
||
176 | $this->logger->notice( |
||
177 | 'The second factor stored in the SSO cookie was revoked or has otherwise became unknown to Gateway', |
||
178 | [ |
||
179 | 'secondFactorIdFromCookie' => $ssoCookie->secondFactorId() |
||
180 | ] |
||
181 | ); |
||
182 | return false; |
||
183 | } |
||
184 | |||
185 | /** @var SecondFactor $secondFactor */ |
||
186 | foreach ($secondFactorCollection as $secondFactor) { |
||
187 | $loa = $secondFactor->getLoaLevel($this->secondFactorTypeService); |
||
188 | if ($loa >= $requiredLoa) { |
||
189 | $this->logger->notice('Verified the current 2FA authentication can be given with the SSO on 2FA cookie'); |
||
190 | $responseContext->saveSelectedSecondFactor($secondFactor); |
||
191 | return true; |
||
192 | } |
||
193 | } |
||
194 | return false; |
||
195 | } |
||
196 | |||
197 | public function getCookieFingerprint(Request $request): string |
||
198 | { |
||
199 | return $this->cookieHelper->fingerprint($request); |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * This method determines if an SSO on 2FA cookie should be created. |
||
204 | * |
||
205 | * The comments in the code block should give a good feel for what business rules |
||
206 | * are applied in this method. |
||
207 | * |
||
208 | * @param CookieValueInterface $ssoCookie The SSO on 2FA cookie as read from the HTTP response |
||
209 | * @param float $loa The LoA that was requested for this authentication, used to |
||
210 | * compare to the LoA stored in the SSO cookie |
||
211 | * @param bool $wasAuthenticatedWithSsoOn2faCookie Indicator if the currently running authentication was performed |
||
212 | * with the SSO on 2FA cookie |
||
213 | */ |
||
214 | private function shouldAddCookie( |
||
215 | CookieValueInterface $ssoCookie, |
||
216 | bool $isCookieValid, |
||
217 | float $loa, |
||
218 | bool $wasAuthenticatedWithSsoOn2faCookie |
||
219 | ): bool { |
||
220 | // When the cookie is not yet set, was expired or was otherwise deemed invalid, we get a NullCookieValue |
||
221 | // back from the reader. Indicating there is no valid cookie present. |
||
222 | $cookieNotSet = $ssoCookie instanceof NullCookieValue; |
||
223 | // OR the existing cookie does exist, but the LoA stored in that cookie does not match the required LoA |
||
224 | $cookieDoesNotMeetLoaRequirement = ($ssoCookie instanceof CookieValue && !$ssoCookie->meetsRequiredLoa($loa)); |
||
225 | if (!$ssoCookie instanceof NullCookieValue && $cookieDoesNotMeetLoaRequirement) { |
||
226 | $this->logger->notice( |
||
227 | sprintf( |
||
228 | 'Storing new SSO on 2FA cookie as LoA requirement (%d changed to %d) changed', |
||
229 | $ssoCookie->getLoa(), |
||
230 | $loa |
||
231 | ) |
||
232 | ); |
||
233 | } |
||
234 | // OR when a new authentication took place, we replace the existing cookie with a new one |
||
235 | if (!$wasAuthenticatedWithSsoOn2faCookie) { |
||
236 | $this->logger->notice('Storing new SSO on 2FA cookie as a new authentication took place'); |
||
237 | } |
||
238 | |||
239 | // Or when the cookie is not valid for some reason (see logs for the specific error) |
||
240 | if (!$isCookieValid) { |
||
241 | $this->logger->notice('Storing new SSO on 2FA cookie, the current cookie is invalid'); |
||
242 | } |
||
243 | |||
244 | return $cookieNotSet || |
||
245 | !$isCookieValid || |
||
246 | $cookieDoesNotMeetLoaRequirement || |
||
247 | !$wasAuthenticatedWithSsoOn2faCookie; |
||
248 | } |
||
249 | |||
250 | private function store(Response $response, CookieValueInterface $cookieValue) |
||
253 | } |
||
254 | |||
255 | private function read(Request $request): CookieValueInterface |
||
256 | { |
||
257 | try { |
||
258 | return $this->cookieHelper->read($request); |
||
259 | } catch (CookieNotFoundException $e) { |
||
260 | $this->logger->notice('Attempt to decrypt the cookie failed, the cookie could not be found'); |
||
261 | return new NullCookieValue(); |
||
262 | } catch (DecryptionFailedException $e) { |
||
263 | $this->logger->notice('Decryption of the SSO on 2FA cookie failed'); |
||
264 | return new NullCookieValue(); |
||
265 | } catch (Exception $e) { |
||
266 | $this->logger->notice( |
||
267 | 'Decryption failed, see original message in context', |
||
268 | ['original-exception-message' => $e->getMessage()] |
||
269 | ); |
||
270 | return new NullCookieValue(); |
||
271 | } |
||
272 | } |
||
273 | |||
274 | private function getRemoteSp(ResponseContext $responseContext): ServiceProvider |
||
275 | { |
||
276 | $remoteSp = $responseContext->getServiceProvider(); |
||
277 | if (!$remoteSp) { |
||
278 | throw new RuntimeException('SP not found in the response context, unable to continue with SSO on 2FA'); |
||
279 | } |
||
280 | return $remoteSp; |
||
281 | } |
||
282 | |||
283 | private function isCookieValid(CookieValueInterface $ssoCookie, float $requiredLoa, string $identityNameId): bool |
||
321 | } |
||
322 | } |
||
323 |