- Introduced `ddev` configuration for Laravel-based SAML SSO setup. - Added detailed technical specifications for SAML 2.0 integration, including endpoints, flows, and cryptographic signing. - Created extensive unit tests to validate `SamlIdpController` and `SamlIdpService` functionalities. - Enhanced metadata generation, SAML request parsing, and cryptographic signing of responses. - Implemented models, services, and tests to standardize IdP interactions with Service Providers.
149 lines
4.8 KiB
PHP
149 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\User;
|
|
use App\Services\SamlIdpService;
|
|
use Illuminate\Http\{Request, Response};
|
|
use Illuminate\Support\Facades\{Auth, Log};
|
|
use Throwable;
|
|
|
|
class SamlIdpController extends Controller
|
|
{
|
|
public function __construct(private readonly SamlIdpService $samlService) {}
|
|
|
|
/**
|
|
* SAML 2.0 Single Sign-On (SSO) Endpoint.
|
|
* Handles both GET (Redirect binding) and POST (POST binding) requests.
|
|
*/
|
|
public function sso(Request $request)
|
|
{
|
|
// Retrieve SAMLRequest and RelayState (from request parameters or session fallback)
|
|
$samlRequest = $request->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 {
|
|
// 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());
|
|
}
|
|
|
|
// 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.');
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
|
|
// Generate Signed SAML Response
|
|
/** @var User $user */
|
|
$user = Auth::user();
|
|
|
|
try {
|
|
$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.');
|
|
}
|
|
|
|
// Clear pending SAML request from session
|
|
session()->forget(['saml_pending_request', 'saml_pending_relay_state']);
|
|
|
|
// 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',
|
|
]);
|
|
}
|
|
}
|