| Total Complexity | 51 |
| Total Lines | 522 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
Complex classes like LoginController 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 LoginController, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 35 | #[AsController] |
||
| 36 | class LoginController |
||
| 37 | { |
||
| 38 | use UrlTrait; |
||
| 39 | use TicketValidatorTrait; |
||
| 40 | |||
| 41 | /** @var \SimpleSAML\Logger */ |
||
| 42 | protected Logger $logger; |
||
| 43 | |||
| 44 | /** @var \SimpleSAML\Configuration */ |
||
| 45 | protected Configuration $casConfig; |
||
| 46 | |||
| 47 | /** @var \SimpleSAML\Module\casserver\Cas\Factories\TicketFactory */ |
||
| 48 | protected TicketFactory $ticketFactory; |
||
| 49 | |||
| 50 | /** @var \SimpleSAML\Auth\Simple */ |
||
| 51 | protected Simple $authSource; |
||
| 52 | |||
| 53 | /** @var \SimpleSAML\Utils\HTTP */ |
||
| 54 | protected Utils\HTTP $httpUtils; |
||
| 55 | |||
| 56 | /** @var \SimpleSAML\Module\casserver\Cas\Protocol\Cas20 */ |
||
| 57 | protected Cas20 $cas20Protocol; |
||
| 58 | |||
| 59 | /** @var \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore */ |
||
| 60 | protected TicketStore $ticketStore; |
||
| 61 | |||
| 62 | /** @var \SimpleSAML\Module\casserver\Cas\ServiceValidator */ |
||
| 63 | protected ServiceValidator $serviceValidator; |
||
| 64 | |||
| 65 | /** @var string[] */ |
||
| 66 | protected array $idpList; |
||
| 67 | |||
| 68 | /** @var string|null */ |
||
| 69 | protected ?string $authProcId = null; |
||
| 70 | |||
| 71 | /** @var string[] */ |
||
| 72 | protected array $postAuthUrlParameters = []; |
||
| 73 | |||
| 74 | /** @var string[] */ |
||
| 75 | private const DEBUG_MODES = ['true', 'samlValidate']; |
||
| 76 | |||
| 77 | /** @var \SimpleSAML\Module\casserver\Cas\AttributeExtractor */ |
||
| 78 | protected AttributeExtractor $attributeExtractor; |
||
| 79 | |||
| 80 | /** @var \SimpleSAML\Module\casserver\Cas\Protocol\SamlValidateResponder */ |
||
| 81 | private SamlValidateResponder $samlValidateResponder; |
||
| 82 | |||
| 83 | /** |
||
| 84 | * @param \SimpleSAML\Configuration $sspConfig |
||
| 85 | * @param \SimpleSAML\Configuration|null $casConfig |
||
| 86 | * @param \SimpleSAML\Auth\Simple|null $source |
||
| 87 | * @param \SimpleSAML\Utils\HTTP|null $httpUtils |
||
| 88 | * |
||
| 89 | * @throws \Exception |
||
| 90 | */ |
||
| 91 | public function __construct( |
||
| 106 | } |
||
| 107 | |||
| 108 | /** |
||
| 109 | * |
||
| 110 | * @param \Symfony\Component\HttpFoundation\Request $request |
||
| 111 | * @param bool $renew |
||
| 112 | * @param bool $gateway |
||
| 113 | * @param string|null $service |
||
| 114 | * @param string|null $TARGET Query parameter name for "service" used by older CAS clients' |
||
| 115 | * @param string|null $scope |
||
| 116 | * @param string|null $language |
||
| 117 | * @param string|null $entityId |
||
| 118 | * @param string|null $debugMode |
||
| 119 | * @param string|null $method |
||
| 120 | * |
||
| 121 | * @return \SimpleSAML\HTTP\RunnableResponse|\SimpleSAML\XHTML\Template |
||
| 122 | * @throws \SimpleSAML\Error\ConfigurationError |
||
| 123 | * @throws \SimpleSAML\Error\NoState |
||
| 124 | */ |
||
| 125 | public function login( |
||
| 126 | Request $request, |
||
| 127 | #[MapQueryParameter] bool $renew = false, |
||
| 128 | #[MapQueryParameter] bool $gateway = false, |
||
| 129 | #[MapQueryParameter] ?string $service = null, |
||
| 130 | #[MapQueryParameter] ?string $TARGET = null, |
||
| 131 | #[MapQueryParameter] ?string $scope = null, |
||
| 132 | #[MapQueryParameter] ?string $language = null, |
||
| 133 | #[MapQueryParameter] ?string $entityId = null, |
||
| 134 | #[MapQueryParameter] ?string $debugMode = null, |
||
| 135 | #[MapQueryParameter] ?string $method = null, |
||
| 136 | ): RunnableResponse|Template { |
||
| 137 | $forceAuthn = $renew; |
||
| 138 | $serviceUrl = $service ?? $TARGET ?? null; |
||
| 139 | $redirect = !(isset($method) && $method === 'POST'); |
||
| 140 | |||
| 141 | // Set initial configurations, or fail |
||
| 142 | $this->handleServiceConfiguration($serviceUrl); |
||
| 143 | |||
| 144 | // Instantiate the classes that rely on the override configuration. |
||
| 145 | // We do not do this in the constructor since we do not have the correct values yet. |
||
| 146 | $this->instantiateClassDependencies(); |
||
| 147 | $this->handleScope($scope); |
||
| 148 | $this->handleLanguage($language); |
||
| 149 | |||
| 150 | // Get the ticket from the session |
||
| 151 | $session = $this->getSession(); |
||
| 152 | $sessionTicket = $this->ticketStore->getTicket($session->getSessionId()); |
||
|
|
|||
| 153 | $sessionRenewId = $sessionTicket['renewId'] ?? null; |
||
| 154 | $requestRenewId = $this->getRequestParam($request, 'renewId'); |
||
| 155 | |||
| 156 | // if this parameter is true, single sign-on will be bypassed and authentication will be enforced |
||
| 157 | $requestForceAuthenticate = $forceAuthn && $sessionRenewId !== $requestRenewId; |
||
| 158 | |||
| 159 | if ($request->query->has(ProcessingChain::AUTHPARAM)) { |
||
| 160 | $this->authProcId = $request->query->get(ProcessingChain::AUTHPARAM); |
||
| 161 | } |
||
| 162 | |||
| 163 | // Construct the ReturnTo URL |
||
| 164 | // This will be used to come back from the AuthSource login or from the Processing Chain |
||
| 165 | $returnToUrl = $this->getReturnUrl($request, $sessionTicket); |
||
| 166 | |||
| 167 | // renew=true and gateway=true are incompatible → prefer interactive login (disable passive) |
||
| 168 | if ($gateway && $forceAuthn) { |
||
| 169 | $gateway = false; |
||
| 170 | } |
||
| 171 | |||
| 172 | // Handle passive authentication if service url defined |
||
| 173 | // Protocol (gateway set): CAS MUST NOT prompt for credentials during this branch. |
||
| 174 | if ($serviceUrl && $gateway && !$this->authSource->isAuthenticated() && !$requestForceAuthenticate) { |
||
| 175 | return $this->handleUnauthenticatedGateway( |
||
| 176 | $serviceUrl, |
||
| 177 | $entityId, |
||
| 178 | $returnToUrl, |
||
| 179 | ); |
||
| 180 | } |
||
| 181 | |||
| 182 | // Handle interactive authentication |
||
| 183 | // Protocol: Normal interactive authentication flow (applies when gateway is not in effect). |
||
| 184 | // Renew semantics: when renew=true, server MUST enforce re-authentication (no SSO reuse). |
||
| 185 | if ( |
||
| 186 | $requestForceAuthenticate || !$this->authSource->isAuthenticated() |
||
| 187 | ) { |
||
| 188 | return $this->handleInteractiveAuthenticate( |
||
| 189 | forceAuthn: $forceAuthn, |
||
| 190 | returnToUrl: $returnToUrl, |
||
| 191 | entityId: $entityId, |
||
| 192 | ); |
||
| 193 | } |
||
| 194 | |||
| 195 | // We are Authenticated. |
||
| 196 | |||
| 197 | $sessionExpiry = $this->authSource->getAuthData('Expire'); |
||
| 198 | // Create a new ticket if we do not have one alreday, or if we are in a forced Authentitcation mode |
||
| 199 | if (!\is_array($sessionTicket) || $forceAuthn) { |
||
| 200 | $sessionTicket = $this->ticketFactory->createSessionTicket($session->getSessionId(), $sessionExpiry); |
||
| 201 | $this->ticketStore->addTicket($sessionTicket); |
||
| 202 | } |
||
| 203 | |||
| 204 | /* We are done. REDIRECT TO LOGGEDIN */ |
||
| 205 | |||
| 206 | if (!isset($serviceUrl) && $this->authProcId === null) { |
||
| 207 | $loggedInUrl = Module::getModuleURL('casserver/loggedIn'); |
||
| 208 | return new RunnableResponse( |
||
| 209 | [$this->httpUtils, 'redirectTrustedURL'], |
||
| 210 | [$loggedInUrl, $this->postAuthUrlParameters], |
||
| 211 | ); |
||
| 212 | } |
||
| 213 | |||
| 214 | // Get the state. |
||
| 215 | $state = $this->getState(); |
||
| 216 | $state['ReturnTo'] = $returnToUrl; |
||
| 217 | if ($this->authProcId !== null) { |
||
| 218 | $state[ProcessingChain::AUTHPARAM] = $this->authProcId; |
||
| 219 | } |
||
| 220 | |||
| 221 | // Attribute Handler |
||
| 222 | $mappedAttributes = $this->attributeExtractor->extractUserAndAttributes($state); |
||
| 223 | $serviceTicket = $this->ticketFactory->createServiceTicket([ |
||
| 224 | 'service' => $serviceUrl, |
||
| 225 | 'forceAuthn' => $forceAuthn, |
||
| 226 | 'userName' => $mappedAttributes['user'], |
||
| 227 | 'attributes' => $mappedAttributes['attributes'], |
||
| 228 | 'proxies' => [], |
||
| 229 | 'sessionId' => $sessionTicket['id'], |
||
| 230 | ]); |
||
| 231 | $this->ticketStore->addTicket($serviceTicket); |
||
| 232 | |||
| 233 | // Check if we are in debug mode. |
||
| 234 | if ($debugMode !== null && $this->casConfig->getOptionalBoolean('debugMode', false)) { |
||
| 235 | [$templateName, $statusCode, $DebugModeXmlString] = $this->handleDebugMode( |
||
| 236 | $request, |
||
| 237 | $debugMode, |
||
| 238 | $serviceTicket, |
||
| 239 | ); |
||
| 240 | $t = new Template($this->sspConfig, (string)$templateName); |
||
| 241 | $t->data['debugMode'] = $debugMode === 'true' ? 'Default' : $debugMode; |
||
| 242 | if (!str_contains('error', (string)$templateName)) { |
||
| 243 | $t->data['DebugModeXml'] = $DebugModeXmlString; |
||
| 244 | } |
||
| 245 | $t->data['statusCode'] = $statusCode; |
||
| 246 | // Return an HTML View that renders the result |
||
| 247 | return $t; |
||
| 248 | } |
||
| 249 | |||
| 250 | // User has SSO or non-interactive auth succeeded → redirect/POST to service WITH a ticket |
||
| 251 | $ticketName = $this->calculateTicketName($service); |
||
| 252 | $this->postAuthUrlParameters[$ticketName] = $serviceTicket['id']; |
||
| 253 | |||
| 254 | // GET |
||
| 255 | if ($redirect) { |
||
| 256 | return new RunnableResponse( |
||
| 257 | [$this->httpUtils, 'redirectTrustedURL'], |
||
| 258 | [$serviceUrl, $this->postAuthUrlParameters], |
||
| 259 | ); |
||
| 260 | } |
||
| 261 | |||
| 262 | // POST |
||
| 263 | return new RunnableResponse( |
||
| 264 | [$this->httpUtils, 'submitPOSTData'], |
||
| 265 | [$serviceUrl, $this->postAuthUrlParameters], |
||
| 266 | ); |
||
| 267 | } |
||
| 268 | |||
| 269 | /** |
||
| 270 | * @param \Symfony\Component\HttpFoundation\Request $request |
||
| 271 | * @param string|null $debugMode |
||
| 272 | * @param array $serviceTicket |
||
| 273 | * |
||
| 274 | * @return array [] |
||
| 275 | */ |
||
| 276 | public function handleDebugMode( |
||
| 277 | Request $request, |
||
| 278 | ?string $debugMode, |
||
| 279 | array $serviceTicket, |
||
| 280 | ): array { |
||
| 281 | // Check if the debugMode is supported |
||
| 282 | if (!in_array($debugMode, self::DEBUG_MODES, true)) { |
||
| 283 | return ['casserver:error.twig', Response::HTTP_BAD_REQUEST, 'Invalid/Unsupported Debug Mode']; |
||
| 284 | } |
||
| 285 | |||
| 286 | if ($debugMode === 'true') { |
||
| 287 | // Service validate CAS20 |
||
| 288 | $xmlResponse = $this->validate( |
||
| 289 | request: $request, |
||
| 290 | method: 'serviceValidate', |
||
| 291 | renew: $request->get('renew', false), |
||
| 292 | target: $request->get('target'), |
||
| 293 | ticket: $serviceTicket['id'], |
||
| 294 | service: $request->get('service'), |
||
| 295 | pgtUrl: $request->get('pgtUrl'), |
||
| 296 | ); |
||
| 297 | return ['casserver:validate.twig', $xmlResponse->getStatusCode(), $xmlResponse->getContent()]; |
||
| 298 | } |
||
| 299 | |||
| 300 | // samlValidate Mode |
||
| 301 | $samlResponse = $this->samlValidateResponder->convertToSaml($serviceTicket); |
||
| 302 | return [ |
||
| 303 | 'casserver:validate.twig', |
||
| 304 | Response::HTTP_OK, |
||
| 305 | (string)$this->samlValidateResponder->wrapInSoap($samlResponse), |
||
| 306 | ]; |
||
| 307 | } |
||
| 308 | |||
| 309 | /** |
||
| 310 | * @return array|null |
||
| 311 | * @throws \SimpleSAML\Error\NoState |
||
| 312 | */ |
||
| 313 | public function getState(): ?array |
||
| 321 | } |
||
| 322 | |||
| 323 | /** |
||
| 324 | * Construct the ticket name |
||
| 325 | * |
||
| 326 | * @param string|null $service |
||
| 327 | * |
||
| 328 | * @return string |
||
| 329 | */ |
||
| 330 | public function calculateTicketName(?string $service): string |
||
| 331 | { |
||
| 332 | $defaultTicketName = $service !== null ? 'ticket' : 'SAMLart'; |
||
| 333 | return $this->casConfig->getOptionalValue('ticketName', $defaultTicketName); |
||
| 334 | } |
||
| 335 | |||
| 336 | /** |
||
| 337 | * @param \Symfony\Component\HttpFoundation\Request $request |
||
| 338 | * @param array|null $sessionTicket |
||
| 339 | * |
||
| 340 | * @return string |
||
| 341 | */ |
||
| 342 | public function getReturnUrl(Request $request, ?array $sessionTicket): string |
||
| 343 | { |
||
| 344 | // Parse the query parameters and return them in an array |
||
| 345 | $query = $this->parseQueryParameters($request, $sessionTicket); |
||
| 346 | // Construct the ReturnTo URL |
||
| 347 | return $this->httpUtils->getSelfURLNoQuery() . '?' . http_build_query($query); |
||
| 348 | } |
||
| 349 | |||
| 350 | /** |
||
| 351 | * @param string|null $serviceUrl |
||
| 352 | * |
||
| 353 | * @return void |
||
| 354 | * @throws \RuntimeException |
||
| 355 | */ |
||
| 356 | public function handleServiceConfiguration(?string $serviceUrl): void |
||
| 372 | } |
||
| 373 | |||
| 374 | /** |
||
| 375 | * @param string|null $language |
||
| 376 | * |
||
| 377 | * @return void |
||
| 378 | */ |
||
| 379 | public function handleLanguage(?string $language): void |
||
| 380 | { |
||
| 381 | // If null, do nothing |
||
| 382 | if ($language === null) { |
||
| 383 | return; |
||
| 384 | } |
||
| 385 | |||
| 386 | $this->postAuthUrlParameters['language'] = $language; |
||
| 387 | } |
||
| 388 | |||
| 389 | /** |
||
| 390 | * @param string|null $scope |
||
| 391 | * |
||
| 392 | * @return void |
||
| 393 | * @throws \RuntimeException |
||
| 394 | */ |
||
| 395 | public function handleScope(?string $scope): void |
||
| 416 | } |
||
| 417 | |||
| 418 | /** |
||
| 419 | * Get the Session |
||
| 420 | * |
||
| 421 | * @return \SimpleSAML\Session|null |
||
| 422 | * @throws \Exception |
||
| 423 | */ |
||
| 424 | public function getSession(): ?Session |
||
| 425 | { |
||
| 426 | return Session::getSessionFromRequest(); |
||
| 427 | } |
||
| 428 | |||
| 429 | /** |
||
| 430 | * @return \SimpleSAML\Module\casserver\Cas\Ticket\TicketStore |
||
| 431 | */ |
||
| 432 | public function getTicketStore(): TicketStore |
||
| 435 | } |
||
| 436 | |||
| 437 | /** |
||
| 438 | * @return void |
||
| 439 | * @throws \Exception |
||
| 440 | */ |
||
| 441 | private function instantiateClassDependencies(): void |
||
| 442 | { |
||
| 443 | $this->cas20Protocol = new Cas20($this->casConfig); |
||
| 444 | |||
| 445 | /* Instantiate ticket factory */ |
||
| 446 | $this->ticketFactory = new TicketFactory($this->casConfig); |
||
| 447 | |||
| 448 | /* Instantiate ticket store */ |
||
| 449 | $ticketStoreConfig = $this->casConfig->getOptionalValue( |
||
| 450 | 'ticketstore', |
||
| 451 | ['class' => 'casserver:FileSystemTicketStore'], |
||
| 452 | ); |
||
| 453 | $ticketStoreClass = Module::resolveClass($ticketStoreConfig['class'], 'Cas\Ticket'); |
||
| 454 | |||
| 455 | // Ticket Store |
||
| 456 | $this->ticketStore = new $ticketStoreClass($this->casConfig); |
||
| 457 | |||
| 458 | // Processing Chain Factory |
||
| 459 | $processingChainFactory = new ProcessingChainFactory($this->casConfig); |
||
| 460 | |||
| 461 | // Attribute Extractor |
||
| 462 | $this->attributeExtractor = new AttributeExtractor($this->casConfig, $processingChainFactory); |
||
| 463 | } |
||
| 464 | |||
| 465 | /** |
||
| 466 | * Trigger interactive authentication via the AuthSource. |
||
| 467 | * |
||
| 468 | * @param bool $forceAuthn |
||
| 469 | * @param string $returnToUrl |
||
| 470 | * @param string|null $entityId |
||
| 471 | * |
||
| 472 | * @return RunnableResponse |
||
| 473 | */ |
||
| 474 | private function handleInteractiveAuthenticate( |
||
| 475 | bool $forceAuthn, |
||
| 476 | string $returnToUrl, |
||
| 477 | ?string $entityId, |
||
| 478 | ): RunnableResponse { |
||
| 479 | return $this->handleAuthenticate( |
||
| 480 | forceAuthn: $forceAuthn, |
||
| 481 | gateway: false, |
||
| 482 | returnToUrl: $returnToUrl, |
||
| 483 | entityId: $entityId, |
||
| 484 | ); |
||
| 485 | } |
||
| 486 | |||
| 487 | /** |
||
| 488 | * Handle the gateway flow when the user is NOT authenticated. |
||
| 489 | * Passive mode is only attempted if 'enable_passive_mode' is enabled in configuration. |
||
| 490 | * |
||
| 491 | * Returns: RunnableResponse|null |
||
| 492 | * - RunnableResponse for either a passive attempt or a redirect to service without ticket. |
||
| 493 | * - null to indicate: proceed with interactive login (non-passive). |
||
| 494 | */ |
||
| 495 | private function handleUnauthenticatedGateway( |
||
| 496 | string $serviceUrl, |
||
| 497 | ?string $entityId, |
||
| 498 | string $returnToUrl, |
||
| 499 | ): RunnableResponse { |
||
| 500 | $passiveAllowed = $this->casConfig->getOptionalBoolean('enable_passive_mode', false); |
||
| 501 | |||
| 502 | // Passive mode is not enabled by configuration |
||
| 503 | // CAS MUST redirect to the service URL WITHOUT a ticket parameter. |
||
| 504 | if (!$passiveAllowed) { |
||
| 505 | return new RunnableResponse( |
||
| 506 | [$this->httpUtils, 'redirectTrustedURL'], |
||
| 507 | [$serviceUrl, []], |
||
| 508 | ); |
||
| 509 | } |
||
| 510 | |||
| 511 | // Passive mode enabled: attempt a passive (non-interactive) authentication. |
||
| 512 | return $this->handleAuthenticate( |
||
| 513 | forceAuthn: false, |
||
| 514 | gateway: true, |
||
| 515 | returnToUrl: $returnToUrl, |
||
| 516 | entityId: $entityId, |
||
| 517 | ); |
||
| 518 | } |
||
| 519 | |||
| 520 | /** |
||
| 521 | * Handle authentication request by configuring parameters and triggering login via auth source. |
||
| 522 | * |
||
| 523 | * @param bool $forceAuthn Whether to force authentication regardless of existing session |
||
| 524 | * @param bool $gateway Whether authentication should be passive/non-interactive |
||
| 525 | * @param string $returnToUrl URL to return to after authentication |
||
| 526 | * @param string|null $entityId Optional specific IdP entity ID to use |
||
| 527 | * |
||
| 528 | * @return RunnableResponse Response containing the login redirect |
||
| 529 | */ |
||
| 530 | private function handleAuthenticate( |
||
| 557 | ); |
||
| 558 | } |
||
| 559 | } |
||
| 560 |