diff --git a/src/IdentityProviderWrapper.php b/src/IdentityProviderWrapper.php index 752be72..b048152 100644 --- a/src/IdentityProviderWrapper.php +++ b/src/IdentityProviderWrapper.php @@ -31,11 +31,15 @@ use Litesaml\Models\Descriptors\Entity; use Litesaml\Models\Descriptors\Idp; use Litesaml\Models\Descriptors\Sp; -use Litesaml\Models\Messages\Attribute; use Litesaml\Models\Messages\AuthnRequest; +use Litesaml\Models\Messages\Context\Attribute; +use Litesaml\Models\Messages\Context\ContextList; +use Litesaml\Models\Messages\Context\NameId; +use Litesaml\Models\Messages\Context\RelayState; +use Litesaml\Models\Messages\Context\SessionIndex; +use Litesaml\Models\Messages\Context\Validate; use Litesaml\Models\Messages\LogoutRequest; use Litesaml\Models\Messages\LogoutResponse; -use Litesaml\Models\Messages\NameId; use Litesaml\Support\MessageHandler; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -77,17 +81,17 @@ public function generateMetadata(): string $entityDescriptor = (new EntityDescriptor($this->idp->entityId)) ->addItem($idpSsoDescriptor); - $context = new SerializationContext(); - $entityDescriptor->serialize($context->getDocument(), $context); + $serializationContext = new SerializationContext(); + $entityDescriptor->serialize($serializationContext->getDocument(), $serializationContext); - return (string) $context->getDocument()->saveXML(); + return (string) $serializationContext->getDocument()->saveXML(); } - /** - * @param Attribute[] $attributes - */ - public function sendAuthnResponse(Sp $recipient, array $attributes, ?NameId $nameId = null): ResponseInterface + public function sendAuthnResponse(Sp $recipient, ContextList $context = new ContextList()): ResponseInterface { + $attributes = $context->all(Attribute::class); + $nameId = $context->first(NameId::class); + $response = new LightSamlAuthnResponse(); $response @@ -170,7 +174,7 @@ public function sendAuthnResponse(Sp $recipient, array $attributes, ?NameId $nam return $this->messageHandler->send($response, $this->idp, $recipient->acs); } - public function handleAuthnRequest(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): AuthnRequest + public function handleAuthnRequest(ServerRequestInterface $request, ContextList $context = new ContextList()): AuthnRequest { $message = $this->messageHandler->unpack($request); @@ -185,13 +189,17 @@ public function handleAuthnRequest(ServerRequestInterface $request, bool $valida nameIdPolicyFormat: $message->getNameIDPolicy()?->getFormat(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - public function sendLogoutRequest(Entity $recipient, NameId $nameId, ?string $relayState = null, ?string $sessionIndex = null): ResponseInterface + public function sendLogoutRequest(Entity $recipient, ContextList $context = new ContextList()): ResponseInterface { + $nameId = $context->required(NameId::class, 'A NameId is required to send a LogoutRequest'); + $relayState = $context->first(RelayState::class)?->value; + $sessionIndex = $context->first(SessionIndex::class)?->value; + $logoutRequest = (new LightSamlLogoutRequest()) ->setID(Helper::generateID()) ->setIssueInstant(new DateTime()) @@ -216,7 +224,7 @@ public function sendLogoutResponse(Entity $recipient): ResponseInterface return $this->messageHandler->send($logoutResponse, $this->idp, $recipient->slo); } - public function handleLogoutRequest(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): LogoutRequest + public function handleLogoutRequest(ServerRequestInterface $request, ContextList $context = new ContextList()): LogoutRequest { $message = $this->messageHandler->unpack($request); @@ -232,12 +240,12 @@ public function handleLogoutRequest(ServerRequestInterface $request, bool $valid relayState: $message->getRelayState(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - public function handleLogoutResponse(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): LogoutResponse + public function handleLogoutResponse(ServerRequestInterface $request, ContextList $context = new ContextList()): LogoutResponse { $message = $this->messageHandler->unpack($request); @@ -251,22 +259,20 @@ public function handleLogoutResponse(ServerRequestInterface $request, bool $vali relayState: $message->getRelayState(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - private function validateIfRequested(SamlMessage $message, bool $validate, ?Entity $issuer): void + private function validateIfRequested(SamlMessage $message, ContextList $context): void { - if (!$validate) { - return; - } + $validate = $context->first(Validate::class); - if ($issuer === null) { - throw new SamlException('An issuer must be provided to validate the signature'); + if ($validate === null) { + return; } - if (!$this->messageHandler->validateSignature($message, $issuer)) { + if (!$this->messageHandler->validateSignature($message, $validate->issuer)) { throw new SamlException('Invalid signature'); } } diff --git a/src/Models/Messages/AuthnResponse.php b/src/Models/Messages/AuthnResponse.php index 429e7e3..1d6f899 100644 --- a/src/Models/Messages/AuthnResponse.php +++ b/src/Models/Messages/AuthnResponse.php @@ -3,6 +3,8 @@ namespace Litesaml\Models\Messages; use Litesaml\Enums\Status; +use Litesaml\Models\Messages\Context\Attribute; +use Litesaml\Models\Messages\Context\NameId; readonly class AuthnResponse extends Message { diff --git a/src/Models/Messages/Attribute.php b/src/Models/Messages/Context/Attribute.php similarity index 70% rename from src/Models/Messages/Attribute.php rename to src/Models/Messages/Context/Attribute.php index 5304396..289601f 100644 --- a/src/Models/Messages/Attribute.php +++ b/src/Models/Messages/Context/Attribute.php @@ -1,8 +1,8 @@ $values diff --git a/src/Models/Messages/Context/Context.php b/src/Models/Messages/Context/Context.php new file mode 100644 index 0000000..831d898 --- /dev/null +++ b/src/Models/Messages/Context/Context.php @@ -0,0 +1,7 @@ +, Context[]> */ + private array $items = []; + + public function __construct(Context ...$items) + { + foreach ($items as $item) { + $this->items[$item::class][] = $item; + } + } + + /** + * @template T of Context + * + * @param class-string $class + * + * @return T[] + */ + public function all(string $class): array + { + return $this->items[$class] ?? []; + } + + /** + * @template T of Context + * + * @param class-string $class + * + * @return T|null + */ + public function first(string $class): ?Context + { + return $this->items[$class][0] ?? null; + } + + /** + * @template T of Context + * + * @param class-string $class + * + * @return T + */ + public function required(string $class, string $message): Context + { + return $this->first($class) ?? throw new SamlException($message); + } +} diff --git a/src/Models/Messages/NameId.php b/src/Models/Messages/Context/NameId.php similarity index 60% rename from src/Models/Messages/NameId.php rename to src/Models/Messages/Context/NameId.php index abe9843..b14771f 100644 --- a/src/Models/Messages/NameId.php +++ b/src/Models/Messages/Context/NameId.php @@ -1,8 +1,8 @@ sp->entityId)) ->addItem($spSsoDescriptor); - $context = new SerializationContext(); - $entityDescriptor->serialize($context->getDocument(), $context); + $serializationContext = new SerializationContext(); + $entityDescriptor->serialize($serializationContext->getDocument(), $serializationContext); - return (string) $context->getDocument()->saveXML(); + return (string) $serializationContext->getDocument()->saveXML(); } - public function sendAuthnRequest(Idp $recipient, ?string $relayState = null, ?string $nameIdPolicyFormat = null): ResponseInterface + public function sendAuthnRequest(Idp $recipient, ContextList $context = new ContextList()): ResponseInterface { + $relayState = $context->first(RelayState::class)?->value; + $nameIdPolicyFormat = $context->first(NameIdPolicyFormat::class)?->value; + $authnRequest = (new LightSamlAuthnRequest()) ->setAssertionConsumerServiceURL($this->sp->acs->location) ->setProtocolBinding($this->sp->acs->getBinding()) @@ -105,7 +113,7 @@ public function sendAuthnRequest(Idp $recipient, ?string $relayState = null, ?st return $this->messageHandler->send($authnRequest, $this->sp, $recipient->sso); } - public function handleAuthnResponse(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): AuthnResponse + public function handleAuthnResponse(ServerRequestInterface $request, ContextList $context = new ContextList()): AuthnResponse { $message = $this->messageHandler->unpack($request); @@ -184,12 +192,12 @@ public function handleAuthnResponse(ServerRequestInterface $request, bool $valid relayState: $message->getRelayState(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - public function handleAuthnRequest(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): AuthnRequest + public function handleAuthnRequest(ServerRequestInterface $request, ContextList $context = new ContextList()): AuthnRequest { $message = $this->messageHandler->unpack($request); @@ -204,13 +212,17 @@ public function handleAuthnRequest(ServerRequestInterface $request, bool $valida nameIdPolicyFormat: $message->getNameIDPolicy()?->getFormat(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - public function sendLogoutRequest(Entity $recipient, NameId $nameId, ?string $relayState = null, ?string $sessionIndex = null): ResponseInterface + public function sendLogoutRequest(Entity $recipient, ContextList $context = new ContextList()): ResponseInterface { + $nameId = $context->required(NameId::class, 'A NameId is required to send a LogoutRequest'); + $relayState = $context->first(RelayState::class)?->value; + $sessionIndex = $context->first(SessionIndex::class)?->value; + $logoutRequest = (new LightSamlLogoutRequest()) ->setID(Helper::generateID()) ->setIssueInstant(new DateTime()) @@ -235,7 +247,7 @@ public function sendLogoutResponse(Entity $recipient): ResponseInterface return $this->messageHandler->send($logoutResponse, $this->sp, $recipient->slo); } - public function handleLogoutRequest(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): LogoutRequest + public function handleLogoutRequest(ServerRequestInterface $request, ContextList $context = new ContextList()): LogoutRequest { $message = $this->messageHandler->unpack($request); @@ -251,12 +263,12 @@ public function handleLogoutRequest(ServerRequestInterface $request, bool $valid relayState: $message->getRelayState(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - public function handleLogoutResponse(ServerRequestInterface $request, bool $validate = false, ?Entity $issuer = null): LogoutResponse + public function handleLogoutResponse(ServerRequestInterface $request, ContextList $context = new ContextList()): LogoutResponse { $message = $this->messageHandler->unpack($request); @@ -270,22 +282,20 @@ public function handleLogoutResponse(ServerRequestInterface $request, bool $vali relayState: $message->getRelayState(), ); - $this->validateIfRequested($message, $validate, $issuer); + $this->validateIfRequested($message, $context); return $dto; } - private function validateIfRequested(SamlMessage $message, bool $validate, ?Entity $issuer): void + private function validateIfRequested(SamlMessage $message, ContextList $context): void { - if (!$validate) { - return; - } + $validate = $context->first(Validate::class); - if ($issuer === null) { - throw new SamlException('An issuer must be provided to validate the signature'); + if ($validate === null) { + return; } - if (!$this->messageHandler->validateSignature($message, $issuer)) { + if (!$this->messageHandler->validateSignature($message, $validate->issuer)) { throw new SamlException('Invalid signature'); } } diff --git a/tests/IdentityProviderWrapperTest.php b/tests/IdentityProviderWrapperTest.php index cfffc83..2f62748 100644 --- a/tests/IdentityProviderWrapperTest.php +++ b/tests/IdentityProviderWrapperTest.php @@ -3,12 +3,14 @@ namespace Tests; use Litesaml\Exceptions\SamlException; -use Litesaml\Models\Messages\Attribute; use Litesaml\Models\Messages\AuthnRequest; use Litesaml\Models\Messages\AuthnResponse; +use Litesaml\Models\Messages\Context\Attribute; +use Litesaml\Models\Messages\Context\ContextList; +use Litesaml\Models\Messages\Context\NameId; +use Litesaml\Models\Messages\Context\Validate; use Litesaml\Models\Messages\LogoutRequest; use Litesaml\Models\Messages\LogoutResponse; -use Litesaml\Models\Messages\NameId; use Litesaml\Support\MetadataParser; use PHPUnit\Framework\Attributes\Test; @@ -71,12 +73,12 @@ public function handle_authn_request_throws_on_wrong_message_type(): void #[Test] public function can_send_authn_response(): void { - $attributes = [ + $context = new ContextList( new Attribute(name: 'email', values: ['user@example.com']), new Attribute(name: 'roles', values: ['admin', 'editor']), - ]; + ); - $response = $this->makeIdpWrapper()->sendAuthnResponse($this->makeSp(), $attributes); + $response = $this->makeIdpWrapper()->sendAuthnResponse($this->makeSp(), $context); $this->assertEquals(302, $response->getStatusCode()); $this->assertStringContainsString('https://sp.localhost/acs', $response->getHeaderLine('Location')); @@ -93,14 +95,13 @@ public function can_send_authn_response(): void #[Test] public function can_send_authn_response_with_name_id(): void { - $attributes = [new Attribute(name: 'email', values: ['user@example.com'])]; - - $response = $this->makeIdpWrapper()->sendAuthnResponse( - $this->makeSp(), - $attributes, + $context = new ContextList( + new Attribute(name: 'email', values: ['user@example.com']), new NameId('user@example.com', 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'), ); + $response = $this->makeIdpWrapper()->sendAuthnResponse($this->makeSp(), $context); + parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $authnResponse = $this->makeSpWrapper()->handleAuthnResponse( $this->makeGetRequest('/acs', $params) @@ -113,12 +114,24 @@ public function can_send_authn_response_with_name_id(): void #[Test] public function can_send_logout_request(): void { - $response = $this->makeIdpWrapper()->sendLogoutRequest($this->makeSp(), new NameId('user@example.com')); + $response = $this->makeIdpWrapper()->sendLogoutRequest( + $this->makeSp(), + new ContextList(new NameId('user@example.com')), + ); $this->assertEquals(302, $response->getStatusCode()); $this->assertStringContainsString('https://sp.localhost/slo', $response->getHeaderLine('Location')); } + #[Test] + public function send_logout_request_throws_when_name_id_missing(): void + { + $this->expectException(SamlException::class); + $this->expectExceptionMessage('A NameId is required to send a LogoutRequest'); + + $this->makeIdpWrapper()->sendLogoutRequest($this->makeSp()); + } + #[Test] public function can_send_logout_response(): void { @@ -148,7 +161,7 @@ public function can_send_logout_request_with_name_id_format(): void { $response = $this->makeIdpWrapper()->sendLogoutRequest( $this->makeSp(), - new NameId('user@example.com', 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'), + new ContextList(new NameId('user@example.com', 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent')), ); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); @@ -173,13 +186,13 @@ public function can_handle_logout_response(): void #[Test] public function send_authn_response_with_encrypted_attribute(): void { - $attributes = [ + $context = new ContextList( new Attribute(name: 'email', values: ['user@example.com']), new Attribute(name: 'roles', values: ['admin'], encrypted: true), - ]; + ); $sp = $this->makeSpWithEncryption(); - $response = $this->makeIdpWrapper()->sendAuthnResponse($sp, $attributes); + $response = $this->makeIdpWrapper()->sendAuthnResponse($sp, $context); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $authnResponse = $this->makeSpWrapper($sp)->handleAuthnResponse( @@ -199,19 +212,9 @@ public function send_authn_response_throws_when_encrypted_attribute_but_no_sp_en $this->expectException(SamlException::class); $this->expectExceptionMessage('No encryption certificate configured on recipient SP'); - $attributes = [new Attribute(name: 'roles', values: ['admin'], encrypted: true)]; + $context = new ContextList(new Attribute(name: 'roles', values: ['admin'], encrypted: true)); - $this->makeIdpWrapper()->sendAuthnResponse($this->makeSp(), $attributes); - } - - #[Test] - public function handle_authn_request_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makeGetRequest('/sso', ['SAMLRequest' => $this->fixture('authn_request')]); - $this->makeIdpWrapper()->handleAuthnRequest($request, validate: true); + $this->makeIdpWrapper()->sendAuthnResponse($this->makeSp(), $context); } #[Test] @@ -221,7 +224,7 @@ public function handle_authn_request_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makeGetRequest('/sso', ['SAMLRequest' => $this->fixture('authn_request')]); - $this->makeIdpWrapper()->handleAuthnRequest($request, validate: true, issuer: $this->makeSp()); + $this->makeIdpWrapper()->handleAuthnRequest($request, new ContextList(new Validate($this->makeSp()))); } #[Test] @@ -233,21 +236,11 @@ public function handle_authn_request_validates_signature_when_requested(): void parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/sso', $params); - $message = $this->makeIdpWrapper()->handleAuthnRequest($request, validate: true, issuer: $this->makeSpWithSigning()); + $message = $this->makeIdpWrapper()->handleAuthnRequest($request, new ContextList(new Validate($this->makeSpWithSigning()))); $this->assertInstanceOf(AuthnRequest::class, $message); } - #[Test] - public function handle_logout_request_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makeGetRequest('/slo', ['SAMLRequest' => $this->fixture('logout_request')]); - $this->makeIdpWrapper()->handleLogoutRequest($request, validate: true); - } - #[Test] public function handle_logout_request_throws_on_invalid_signature(): void { @@ -255,7 +248,7 @@ public function handle_logout_request_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makeGetRequest('/slo', ['SAMLRequest' => $this->fixture('logout_request')]); - $this->makeIdpWrapper()->handleLogoutRequest($request, validate: true, issuer: $this->makeSp()); + $this->makeIdpWrapper()->handleLogoutRequest($request, new ContextList(new Validate($this->makeSp()))); } #[Test] @@ -263,25 +256,15 @@ public function handle_logout_request_validates_signature_when_requested(): void { $spWithSigning = $this->makeSpWrapper($this->makeSpWithSigning()); - $response = $spWithSigning->sendLogoutRequest($this->makeIdp(), new NameId('user@example.com')); + $response = $spWithSigning->sendLogoutRequest($this->makeIdp(), new ContextList(new NameId('user@example.com'))); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/slo', $params); - $message = $this->makeIdpWrapper()->handleLogoutRequest($request, validate: true, issuer: $this->makeSpWithSigning()); + $message = $this->makeIdpWrapper()->handleLogoutRequest($request, new ContextList(new Validate($this->makeSpWithSigning()))); $this->assertInstanceOf(LogoutRequest::class, $message); } - #[Test] - public function handle_logout_response_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makeGetRequest('/slo', ['SAMLResponse' => $this->fixture('logout_response')]); - $this->makeIdpWrapper()->handleLogoutResponse($request, validate: true); - } - #[Test] public function handle_logout_response_throws_on_invalid_signature(): void { @@ -289,7 +272,7 @@ public function handle_logout_response_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makeGetRequest('/slo', ['SAMLResponse' => $this->fixture('logout_response')]); - $this->makeIdpWrapper()->handleLogoutResponse($request, validate: true, issuer: $this->makeSp()); + $this->makeIdpWrapper()->handleLogoutResponse($request, new ContextList(new Validate($this->makeSp()))); } #[Test] @@ -319,7 +302,7 @@ public function handle_logout_response_validates_signature_when_requested(): voi parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/slo', $params); - $message = $this->makeIdpWrapper()->handleLogoutResponse($request, validate: true, issuer: $this->makeSpWithSigning()); + $message = $this->makeIdpWrapper()->handleLogoutResponse($request, new ContextList(new Validate($this->makeSpWithSigning()))); $this->assertInstanceOf(LogoutResponse::class, $message); } diff --git a/tests/ServiceProviderWrapperTest.php b/tests/ServiceProviderWrapperTest.php index 7c00a1c..75ba201 100644 --- a/tests/ServiceProviderWrapperTest.php +++ b/tests/ServiceProviderWrapperTest.php @@ -5,12 +5,16 @@ use LightSaml\Binding\SamlPostResponse; use Litesaml\Enums\Status; use Litesaml\Exceptions\SamlException; -use Litesaml\Models\Messages\Attribute; use Litesaml\Models\Messages\AuthnRequest; use Litesaml\Models\Messages\AuthnResponse; +use Litesaml\Models\Messages\Context\Attribute; +use Litesaml\Models\Messages\Context\ContextList; +use Litesaml\Models\Messages\Context\NameId; +use Litesaml\Models\Messages\Context\NameIdPolicyFormat; +use Litesaml\Models\Messages\Context\RelayState; +use Litesaml\Models\Messages\Context\Validate; use Litesaml\Models\Messages\LogoutRequest; use Litesaml\Models\Messages\LogoutResponse; -use Litesaml\Models\Messages\NameId; use Litesaml\Support\MetadataParser; use PHPUnit\Framework\Attributes\Test; @@ -28,12 +32,12 @@ public function generate_metadata_includes_encryption_key_descriptor(): void public function handle_authn_response_decrypts_encrypted_assertions(): void { $sp = $this->makeSpWithEncryption(); - $attributes = [ + $context = new ContextList( new Attribute(name: 'email', values: ['user@example.com']), new Attribute(name: 'roles', values: ['admin'], encrypted: true), - ]; + ); - $response = $this->makeIdpWrapper()->sendAuthnResponse($sp, $attributes); + $response = $this->makeIdpWrapper()->sendAuthnResponse($sp, $context); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $message = $this->makeSpWrapper($sp)->handleAuthnResponse($this->makeGetRequest('/acs', $params)); @@ -51,8 +55,8 @@ public function handle_authn_response_throws_when_encrypted_assertion_without_sp $this->expectExceptionMessage('No encryption certificate configured to decrypt assertion'); $sp = $this->makeSpWithEncryption(); - $attributes = [new Attribute(name: 'roles', values: ['admin'], encrypted: true)]; - $response = $this->makeIdpWrapper()->sendAuthnResponse($sp, $attributes); + $context = new ContextList(new Attribute(name: 'roles', values: ['admin'], encrypted: true)); + $response = $this->makeIdpWrapper()->sendAuthnResponse($sp, $context); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $this->makeSpWrapper()->handleAuthnResponse($this->makeGetRequest('/acs', $params)); @@ -104,7 +108,7 @@ public function send_authn_request_includes_name_id_policy_format(): void { $response = $this->makeSpWrapper()->sendAuthnRequest( $this->makeIdp(), - nameIdPolicyFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + new ContextList(new NameIdPolicyFormat('urn:oasis:names:tc:SAML:2.0:nameid-format:persistent')), ); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); @@ -116,7 +120,7 @@ public function send_authn_request_includes_name_id_policy_format(): void #[Test] public function send_authn_request_includes_relay_state(): void { - $response = $this->makeSpWrapper()->sendAuthnRequest($this->makeIdp(), 'my-relay-state'); + $response = $this->makeSpWrapper()->sendAuthnRequest($this->makeIdp(), new ContextList(new RelayState('my-relay-state'))); $this->assertStringContainsString('RelayState=my-relay-state', $response->getHeaderLine('Location')); } @@ -197,16 +201,31 @@ public function can_handle_authn_request(): void #[Test] public function can_send_logout_request(): void { - $response = $this->makeSpWrapper()->sendLogoutRequest($this->makeIdp(), new NameId('user@example.com')); + $response = $this->makeSpWrapper()->sendLogoutRequest( + $this->makeIdp(), + new ContextList(new NameId('user@example.com')), + ); $this->assertEquals(302, $response->getStatusCode()); $this->assertStringContainsString('https://idp.localhost/slo', $response->getHeaderLine('Location')); } + #[Test] + public function send_logout_request_throws_when_name_id_missing(): void + { + $this->expectException(SamlException::class); + $this->expectExceptionMessage('A NameId is required to send a LogoutRequest'); + + $this->makeSpWrapper()->sendLogoutRequest($this->makeIdp()); + } + #[Test] public function send_logout_request_includes_relay_state(): void { - $response = $this->makeSpWrapper()->sendLogoutRequest($this->makeIdp(), new NameId('user@example.com'), 'my-relay-state'); + $response = $this->makeSpWrapper()->sendLogoutRequest( + $this->makeIdp(), + new ContextList(new NameId('user@example.com'), new RelayState('my-relay-state')), + ); $this->assertStringContainsString('RelayState=my-relay-state', $response->getHeaderLine('Location')); } @@ -240,7 +259,7 @@ public function can_send_logout_request_with_name_id_format(): void { $response = $this->makeSpWrapper()->sendLogoutRequest( $this->makeIdp(), - new NameId('user@example.com', 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'), + new ContextList(new NameId('user@example.com', 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent')), ); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); @@ -262,16 +281,6 @@ public function can_handle_logout_response(): void $this->assertEquals('https://idp.localhost', $message->issuer); } - #[Test] - public function handle_authn_response_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makePostRequest('/acs', ['SAMLResponse' => $this->fixture('authn_response', deflate: false)]); - $this->makeSpWrapper()->handleAuthnResponse($request, validate: true); - } - #[Test] public function handle_authn_response_throws_on_invalid_signature(): void { @@ -279,20 +288,20 @@ public function handle_authn_response_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makePostRequest('/acs', ['SAMLResponse' => $this->fixture('authn_response', deflate: false)]); - $this->makeSpWrapper()->handleAuthnResponse($request, validate: true, issuer: $this->makeIdp()); + $this->makeSpWrapper()->handleAuthnResponse($request, new ContextList(new Validate($this->makeIdp()))); } #[Test] public function handle_authn_response_validates_signature_when_requested(): void { $idpWithSigning = $this->makeIdpWrapper($this->makeIdpWithSigning()); - $attributes = [new Attribute(name: 'email', values: ['user@example.com'])]; + $context = new ContextList(new Attribute(name: 'email', values: ['user@example.com'])); - $response = $idpWithSigning->sendAuthnResponse($this->makeSp(), $attributes); + $response = $idpWithSigning->sendAuthnResponse($this->makeSp(), $context); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/acs', $params); - $message = $this->makeSpWrapper()->handleAuthnResponse($request, validate: true, issuer: $this->makeIdpWithSigning()); + $message = $this->makeSpWrapper()->handleAuthnResponse($request, new ContextList(new Validate($this->makeIdpWithSigning()))); $this->assertInstanceOf(AuthnResponse::class, $message); } @@ -302,14 +311,14 @@ public function handle_authn_response_validates_signature_for_post_binding(): vo { $idpWithSigning = $this->makeIdpWrapper($this->makeIdpWithSigning()); $sp = $this->makeSpWithPostAcs(); - $attributes = [new Attribute(name: 'email', values: ['user@example.com'])]; + $context = new ContextList(new Attribute(name: 'email', values: ['user@example.com'])); - $response = $idpWithSigning->sendAuthnResponse($sp, $attributes); + $response = $idpWithSigning->sendAuthnResponse($sp, $context); $this->assertInstanceOf(SamlPostResponse::class, $response); $request = $this->makePostRequest('/acs', $response->getData()); - $message = $this->makeSpWrapper($sp)->handleAuthnResponse($request, validate: true, issuer: $this->makeIdpWithSigning()); + $message = $this->makeSpWrapper($sp)->handleAuthnResponse($request, new ContextList(new Validate($this->makeIdpWithSigning()))); $this->assertInstanceOf(AuthnResponse::class, $message); } @@ -322,9 +331,9 @@ public function handle_authn_response_throws_on_tampered_post_binding_signature( $idpWithSigning = $this->makeIdpWrapper($this->makeIdpWithSigning()); $sp = $this->makeSpWithPostAcs(); - $attributes = [new Attribute(name: 'email', values: ['user@example.com'])]; + $context = new ContextList(new Attribute(name: 'email', values: ['user@example.com'])); - $response = $idpWithSigning->sendAuthnResponse($sp, $attributes); + $response = $idpWithSigning->sendAuthnResponse($sp, $context); $this->assertInstanceOf(SamlPostResponse::class, $response); $data = $response->getData(); @@ -333,7 +342,7 @@ public function handle_authn_response_throws_on_tampered_post_binding_signature( $request = $this->makePostRequest('/acs', $data); - $this->makeSpWrapper($sp)->handleAuthnResponse($request, validate: true, issuer: $this->makeIdpWithSigning()); + $this->makeSpWrapper($sp)->handleAuthnResponse($request, new ContextList(new Validate($this->makeIdpWithSigning()))); } #[Test] @@ -347,9 +356,9 @@ public function handle_authn_response_rejects_xml_signature_wrapping_attack(): v $idpWithSigning = $this->makeIdpWrapper($this->makeIdpWithSigning()); $sp = $this->makeSpWithPostAcs(); - $attributes = [new Attribute(name: 'email', values: ['user@example.com'])]; + $context = new ContextList(new Attribute(name: 'email', values: ['user@example.com'])); - $response = $idpWithSigning->sendAuthnResponse($sp, $attributes); + $response = $idpWithSigning->sendAuthnResponse($sp, $context); $this->assertInstanceOf(SamlPostResponse::class, $response); $data = $response->getData(); @@ -372,17 +381,7 @@ public function handle_authn_response_rejects_xml_signature_wrapping_attack(): v ); $request = $this->makePostRequest('/acs', ['SAMLResponse' => base64_encode($wrapped)]); - $this->makeSpWrapper($sp)->handleAuthnResponse($request, validate: true, issuer: $this->makeIdpWithSigning()); - } - - #[Test] - public function handle_authn_request_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makeGetRequest('/sso', ['SAMLRequest' => $this->fixture('authn_request')]); - $this->makeSpWrapper()->handleAuthnRequest($request, validate: true); + $this->makeSpWrapper($sp)->handleAuthnResponse($request, new ContextList(new Validate($this->makeIdpWithSigning()))); } #[Test] @@ -392,7 +391,7 @@ public function handle_authn_request_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makeGetRequest('/sso', ['SAMLRequest' => $this->fixture('authn_request')]); - $this->makeSpWrapper()->handleAuthnRequest($request, validate: true, issuer: $this->makeSp()); + $this->makeSpWrapper()->handleAuthnRequest($request, new ContextList(new Validate($this->makeSp()))); } #[Test] @@ -404,7 +403,7 @@ public function handle_authn_request_validates_signature_when_requested(): void parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/sso', $params); - $message = $this->makeSpWrapper()->handleAuthnRequest($request, validate: true, issuer: $this->makeSpWithSigning()); + $message = $this->makeSpWrapper()->handleAuthnRequest($request, new ContextList(new Validate($this->makeSpWithSigning()))); $this->assertInstanceOf(AuthnRequest::class, $message); } @@ -427,16 +426,6 @@ public function handle_logout_response_throws_on_wrong_message_type(): void $this->makeSpWrapper()->handleLogoutResponse($request); } - #[Test] - public function handle_logout_request_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makeGetRequest('/slo', ['SAMLRequest' => $this->fixture('logout_request')]); - $this->makeSpWrapper()->handleLogoutRequest($request, validate: true); - } - #[Test] public function handle_logout_request_throws_on_invalid_signature(): void { @@ -444,7 +433,7 @@ public function handle_logout_request_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makeGetRequest('/slo', ['SAMLRequest' => $this->fixture('logout_request')]); - $this->makeSpWrapper()->handleLogoutRequest($request, validate: true, issuer: $this->makeIdp()); + $this->makeSpWrapper()->handleLogoutRequest($request, new ContextList(new Validate($this->makeIdp()))); } #[Test] @@ -452,25 +441,15 @@ public function handle_logout_request_validates_signature_when_requested(): void { $idpWithSigning = $this->makeIdpWrapper($this->makeIdpWithSigning()); - $response = $idpWithSigning->sendLogoutRequest($this->makeSp(), new NameId('user@example.com')); + $response = $idpWithSigning->sendLogoutRequest($this->makeSp(), new ContextList(new NameId('user@example.com'))); parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/slo', $params); - $message = $this->makeSpWrapper()->handleLogoutRequest($request, validate: true, issuer: $this->makeIdpWithSigning()); + $message = $this->makeSpWrapper()->handleLogoutRequest($request, new ContextList(new Validate($this->makeIdpWithSigning()))); $this->assertInstanceOf(LogoutRequest::class, $message); } - #[Test] - public function handle_logout_response_throws_when_validate_requires_issuer(): void - { - $this->expectException(SamlException::class); - $this->expectExceptionMessage('An issuer must be provided to validate the signature'); - - $request = $this->makeGetRequest('/slo', ['SAMLResponse' => $this->fixture('logout_response')]); - $this->makeSpWrapper()->handleLogoutResponse($request, validate: true); - } - #[Test] public function handle_logout_response_throws_on_invalid_signature(): void { @@ -478,7 +457,7 @@ public function handle_logout_response_throws_on_invalid_signature(): void $this->expectExceptionMessage('Invalid signature'); $request = $this->makeGetRequest('/slo', ['SAMLResponse' => $this->fixture('logout_response')]); - $this->makeSpWrapper()->handleLogoutResponse($request, validate: true, issuer: $this->makeIdp()); + $this->makeSpWrapper()->handleLogoutResponse($request, new ContextList(new Validate($this->makeIdp()))); } #[Test] @@ -490,7 +469,7 @@ public function handle_logout_response_validates_signature_when_requested(): voi parse_str((string) parse_url($response->getHeaderLine('Location'), PHP_URL_QUERY), $params); $request = $this->makeGetRequest('/slo', $params); - $message = $this->makeSpWrapper()->handleLogoutResponse($request, validate: true, issuer: $this->makeIdpWithSigning()); + $message = $this->makeSpWrapper()->handleLogoutResponse($request, new ContextList(new Validate($this->makeIdpWithSigning()))); $this->assertInstanceOf(LogoutResponse::class, $message); }