singleloginsystem/tests/Feature/SamlIdpTest.php
= 4849736c60 Feat: add intials SAML SSO compatibility.
- SAML public certificate generation
- Authentication using SAML
2026-05-27 05:49:36 +00:00

114 lines
4.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\{ConnectedApp, ConnectionProtocol, ConnectionProvider, ConnectionStatus, User};
use Illuminate\Support\Facades\File;
beforeEach(function (): void {
// Generate testing keys if not already present
$directory = storage_path('app/saml');
if (! File::exists($directory)) {
File::makeDirectory($directory, 0755, true);
}
if (! File::exists("{$directory}/idp.key") || ! File::exists("{$directory}/idp.crt")) {
$this->artisan('saml:generate-keys');
}
$this->seed(Database\Seeders\DatabaseSeeder::class);
// Get seeded protocol, provider, and status
$this->protocol = ConnectionProtocol::query()->where('name', 'saml')->firstOrFail();
$this->provider = ConnectionProvider::query()->where('slug', 'azure-fd')->firstOrFail();
$this->status = ConnectionStatus::query()->where('name', 'connected')->firstOrFail();
// Create a mock SAML App
$this->samlApp = ConnectedApp::query()->create([
'name' => 'Microsoft Office 365',
'slug' => 'microsoft-office-365',
'connection_protocol_id' => $this->protocol->id,
'connection_provider_id' => $this->provider->id,
'connection_status_id' => $this->status->id,
'settings' => [
'saml' => [
'entity_id' => 'urn:federation:MicrosoftOnline',
'acs_url' => 'https://login.microsoftonline.com/login.srf',
],
],
]);
// Create a mock User
$this->user = User::factory()->create([
'email' => 'testuser@company.com',
'immutable_id' => 'user-123-immutable',
]);
});
test('saml metadata endpoint returns active public certificate and sso location', function (): void {
$response = $this->get(route('saml.metadata'));
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'application/xml; charset=utf-8');
$content = $response->getContent();
expect($content)->toContain('EntityDescriptor')
->toContain('IDPSSODescriptor')
->toContain('SingleSignOnService')
->toContain(route('saml.sso'))
->toContain('X509Certificate');
});
test('saml sso endpoint aborts if missing SAMLRequest', function (): void {
$response = $this->get(route('saml.sso'));
$response->assertStatus(400);
});
test('saml sso endpoint redirects unauthenticated users to login page and remembers request', function (): void {
$xml = '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_mock_request_id" Version="2.0" IssueInstant="2026-05-26T12:00:00Z" AssertionConsumerServiceURL="https://login.microsoftonline.com/login.srf" Destination="http://localhost/saml/sso"><saml:Issuer>urn:federation:MicrosoftOnline</saml:Issuer></samlp:AuthnRequest>';
$samlRequest = base64_encode(gzdeflate($xml));
$relayState = 'https://teams.microsoft.com/';
$response = $this->get(route('saml.sso', [
'SAMLRequest' => $samlRequest,
'RelayState' => $relayState,
]));
$response->assertRedirect(route('login'));
// Assert session retains pending details
expect(session('saml_pending_request'))->toBe($samlRequest);
expect(session('saml_pending_relay_state'))->toBe($relayState);
});
test('saml sso endpoint signs and redirects authenticated users via POST binding', function (): void {
$xml = '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_mock_request_id" Version="2.0" IssueInstant="2026-05-26T12:00:00Z" AssertionConsumerServiceURL="https://login.microsoftonline.com/login.srf" Destination="http://localhost/saml/sso"><saml:Issuer>urn:federation:MicrosoftOnline</saml:Issuer></samlp:AuthnRequest>';
$samlRequest = base64_encode(gzdeflate($xml));
$relayState = 'https://teams.microsoft.com/';
// Log in the user
$this->actingAs($this->user);
$response = $this->get(route('saml.sso', [
'SAMLRequest' => $samlRequest,
'RelayState' => $relayState,
]));
$response->assertStatus(200);
$response->assertViewIs('saml.post_response');
$response->assertViewHasAll([
'appName' => 'Microsoft Office 365',
'acsUrl' => 'https://login.microsoftonline.com/login.srf',
'relayState' => $relayState,
]);
$content = $response->getContent();
expect($content)->toContain('name="SAMLResponse"')
->toContain('name="RelayState"')
->toContain('value="'.$relayState.'"');
// Verify session was cleared
expect(session('saml_pending_request'))->toBeNull();
});