input('SAMLRequest') ?? session('saml_pending_request'); $relayState = $request->input('RelayState') ?? session('saml_pending_relay_state'); if (empty($samlRequest)) { Log::warning('SAML SSO accessed without a SAMLRequest.'); abort(400, 'Missing SAMLRequest parameter.'); } try { // 2. Parse the SAMLRequest XML $parsed = $this->samlService->parseRequest($samlRequest); } catch (Throwable $e) { Log::error('Failed to parse SAMLRequest: '.$e->getMessage()); abort(400, 'Invalid SAMLRequest: '.$e->getMessage()); } // 3. Find corresponding Connected App by issuer (SAML Entity ID) $connectedApp = $this->samlService->findConnectedApp($parsed['issuer']); if (! $connectedApp) { Log::warning( "SAML SP Issuer [{$parsed['issuer']}] is not registered.", ); abort( 403, "The Service Provider [{$parsed['issuer']}] is not authorized in this system.", ); } if ('disconnected' === $connectedApp->status?->name) { Log::warning( "SAML SP Issuer [{$parsed['issuer']}] is registered but disconnected.", ); abort( 403, "The Service Provider [{$connectedApp->name}] is currently deactivated.", ); } /** * @var array{ * saml?: array{ * entity_id?: string, * acs_url?: string * } * } $settings */ $settings = $connectedApp->settings; // Use the ACS URL from the request, falling back to the registered ACS URL $acsUrl = $parsed['acsUrl'] ?: $settings['saml']['acs_url'] ?? null; if (empty($acsUrl)) { Log::error( "No Assertion Consumer Service (ACS) URL defined for [{$connectedApp->name}].", ); abort(400, 'Assertion Consumer Service (ACS) URL is not defined.'); } // 4. Authenticate the User if (! Auth::check()) { // Store the SAML details in session to complete flow post-login session([ 'saml_pending_request' => $samlRequest, 'saml_pending_relay_state' => $relayState, ]); return redirect()->route('login'); } // 5. Generate Signed SAML Response $user = Auth::user(); try { /** @var \App\Models\User $user */ $samlResponse = $this->samlService->generateResponse( $user, $connectedApp, $parsed['requestId'], $acsUrl, ); } catch (Throwable $e) { Log::error('Failed to generate SAML Response: '.$e->getMessage()); abort(500, 'Cryptographic signing of SAML Response failed.'); } // 6. Clear pending SAML request from session session()->forget(['saml_pending_request', 'saml_pending_relay_state']); // 7. Return HTTP-POST Auto-Submission Page return view('saml.post_response', [ 'appName' => $connectedApp->name, 'acsUrl' => $acsUrl, 'samlResponse' => $samlResponse, 'relayState' => $relayState, ]); } /** * SAML 2.0 Identity Provider (IdP) Metadata Endpoint. * Exposes public keys and bindings to Service Providers like Microsoft Entra ID. */ public function metadata(): Response { $idpEntityId = route('saml.metadata'); $ssoUrl = route('saml.sso'); try { $metadataXml = $this->samlService->generateMetadata( $idpEntityId, $ssoUrl, ); } catch (Throwable $e) { Log::error( 'Failed to generate SAML IdP Metadata: '.$e->getMessage(), ); abort( 500, 'Failed to retrieve Identity Provider cryptographic identity.', ); } return response($metadataXml, 200, [ 'Content-Type' => 'application/xml; charset=utf-8', ]); } }