singleloginsystem/app/Http/Controllers/SamlIdpController.php
= 1a7134419c Refactor: Improve dependency injection for dashboard components and enhance SAML logging
- Removed inline data definitions from multiple dashboard components for roles, users, and connected apps, replacing them with service-based computed properties.
- Added comprehensive debug and error-level logging throughout `SamlIdpService` and related controllers to improve traceability of SAML flows.
- Cleaned up and centralized SAML-related logic, improving maintainability and error handling consistency.
- Deleted unused `hr-permission.blade.php` component to reduce code redundancy.
2026-06-01 13:05:59 +00:00

212 lines
7.3 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)
{
$samlRequest = $request->input('SAMLRequest') ?? session('saml_pending_request');
$relayState = $request->input('RelayState') ?? session('saml_pending_relay_state');
Log::info('SAML SSO Request received.', [
'ip' => $request->ip(),
'method' => $request->method(),
'has_saml_request' => ! empty($samlRequest),
'has_relay_state' => ! empty($relayState),
'session_has_pending' => session()->has('saml_pending_request'),
]);
if (empty($samlRequest)) {
Log::warning('SAML SSO access rejected: Missing SAMLRequest parameter.', [
'ip' => $request->ip(),
]);
abort(400, 'Missing SAMLRequest parameter.');
}
try {
Log::info('Decoding and parsing SAMLRequest XML payload.');
$parsed = $this->samlService->parseRequest($samlRequest);
Log::info('SAMLRequest successfully parsed.', [
'request_id' => $parsed['requestId'],
'issuer' => $parsed['issuer'],
'acs_url' => $parsed['acsUrl'],
]);
} catch (Throwable $e) {
Log::error('SAMLRequest parsing failed.', [
'ip' => $request->ip(),
'error' => $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 SSO access rejected: Unregistered Service Provider (EntityID / Issuer).', [
'issuer' => $parsed['issuer'],
'request_id' => $parsed['requestId'],
'ip' => $request->ip(),
]);
abort(
403,
"The Service Provider [{$parsed['issuer']}] is not authorized in this system.",
);
}
Log::info('SAML ConnectedApp matched successfully.', [
'app_id' => $connectedApp->id,
'app_name' => $connectedApp->name,
'status' => $connectedApp->status?->name,
]);
if ('disconnected' === $connectedApp->status?->name) {
Log::warning('SAML SSO access rejected: Connected App is deactivated.', [
'app_id' => $connectedApp->id,
'app_name' => $connectedApp->name,
'request_id' => $parsed['requestId'],
]);
abort(
403,
"The Service Provider [{$connectedApp->name}] is currently deactivated.",
);
}
/**
* @var array{
* saml?: array{
* entity_id?: string,
* acs_url?: string
* }
* } $settings
*/
$settings = $connectedApp->settings;
$acsUrl = $parsed['acsUrl'] ?: $settings['saml']['acs_url'] ?? null;
if (empty($acsUrl)) {
Log::error('SAML SSO failed: Assertion Consumer Service (ACS) URL is undefined.', [
'app_id' => $connectedApp->id,
'app_name' => $connectedApp->name,
'request_id' => $parsed['requestId'],
]);
abort(400, 'Assertion Consumer Service (ACS) URL is not defined.');
}
// Authenticate the User
if (! Auth::check()) {
Log::info('SAML SSO guest session intercepted. Persisting request parameters to session and redirecting to login.', [
'request_id' => $parsed['requestId'],
'relay_state' => $relayState,
'ip' => $request->ip(),
]);
session([
'saml_pending_request' => $samlRequest,
'saml_pending_relay_state' => $relayState,
]);
return redirect()->route('login');
}
/** @var User $user */
$user = Auth::user();
Log::info('SAML SSO user authenticated session detected. Generating signed SAML Response assertion.', [
'user_id' => $user->id,
'user_email' => $user->email,
'app_id' => $connectedApp->id,
'app_name' => $connectedApp->name,
'acs_url' => $acsUrl,
'request_id' => $parsed['requestId'],
]);
try {
$samlResponse = $this->samlService->generateResponse(
$user,
$connectedApp,
$parsed['requestId'],
$acsUrl,
);
Log::info('Signed SAML Response assertion successfully generated.');
} catch (Throwable $e) {
Log::error('SAML Response generation failed.', [
'user_id' => $user->id,
'app_id' => $connectedApp->id,
'error' => $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']);
Log::info('Pending SAML session parameters cleared.');
Log::info('Rendering SAML HTTP-POST auto-submission redirect form.', [
'user_id' => $user->id,
'app_name' => $connectedApp->name,
'acs_url' => $acsUrl,
'relay_state' => $relayState,
]);
// 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');
Log::info('SAML IdP Metadata requested.', [
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'entity_id' => $idpEntityId,
'sso_url' => $ssoUrl,
]);
try {
$metadataXml = $this->samlService->generateMetadata(
$idpEntityId,
$ssoUrl,
);
Log::info('SAML IdP Metadata XML descriptor generated successfully.');
} catch (Throwable $e) {
Log::error('SAML IdP Metadata XML generation failed.', [
'error' => $e->getMessage(),
]);
abort(
500,
'Failed to retrieve Identity Provider cryptographic identity.',
);
}
return response($metadataXml, 200, [
'Content-Type' => 'application/xml; charset=utf-8',
]);
}
}