Skip to content

Commit

Permalink
First work
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed May 12, 2024
1 parent 3a05016 commit 2bc4ad3
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 12 deletions.
60 changes: 60 additions & 0 deletions src/SAML2/Artifact.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2;

/**
* Class for SAML artifacts.
*
* @package simplesamlphp/saml2
*/
final class Artifact
{
/**
* Initialize an artifact.
*
* @param string $artifact
* @param int $endpointIndex
* @param string $sourceId
*/
public function __construct(
protected string $artifact,
protected int $endpointIndex,
protected string $sourceId,
) {
}


/**
* Collect the value of the artifact-property
*
* @return string
*/
public function getArtifact: string
{
return $this->artifact;
}


/**
* Collect the value of the endpointIndex-property
*
* @return int
*/
public function getEndpointIndex(): int
{
return $this->endpointIndex;
}


/**
* Collect the value of the sourceId-property
*
* @return string
*/
public function getSourceId(): string
{
return $this->sourceId;
}
}
170 changes: 170 additions & 0 deletions src/SAML2/Entity/ServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\Entity;

use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\SAML2\Exception\MetadataNotFoundException;
use SimpleSAML\SAML2\Exception\Protocol\ResourceNotRecognizedException;
use SimpleSAML\SAML2\Exception\RemoteException;
use SimpleSAML\SAML2\Metadata;
use SimpleSAML\SAML2\XML\samlp\Response;

/**
* Class representing a SAML 2 Service Provider.
*
* @package simplesamlphp/saml2
*/
abstract class ServiceProvider
{
protected ?Response $validatedResponse;


/**
* @param \SimpleSAML\SAML2\Metadata\ServiceProvider $spMetadata
* @param bool $encryptedAssertions Whether assertions must be encrypted
* @param bool $disableScoping Wheter to send the samlp:Scoping element in requests
* @param bool $enableUnsolicited Wheter to process unsolicited responses
* @param bool $encryptNameId Whether to encrypt the NameID sent
* @param bool $signAuthnRequest Whether to sign the AuthnRequest sent
* @param bool $signLogout Whether to sign the LogoutRequest/LogoutResponse sent
* @param bool $validateLogout Whether to validate the signature of LogoutRequest/LogoutResponse received
*/
public function __construct(
protected readonly Metadata\ServiceProvider $spMetadata,
protected readonly bool $encryptedAssertions = false,
protected readonly bool $disableScoping = false,
protected readonly bool $enableUnsolicited = false,
protected readonly bool $encryptNameId = false,
protected readonly bool $signAuthnRequest = false,
protected readonly bool $signLogout = false,
protected readonly bool $validateLogout = true,
) {
}


/**
* Receive a validated response.
*
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return \SimpleSAML\SAML2\XML\samlp\Response The validated response.
*
* @throws \SimpleSAML\SAML2\Exception\Protocol\UnsupportedBindingException
*/
public function receiveValidatedResponse(ServerRequestInterface $request): Response
{
$b = Binding::getCurrentBinding($request);

if ($b instanceof HTTPArtifact) {
$artifact = $b->receiveArtifact($request);
$idpMetadata = $this->getMetadataForSha1($artifact->getSourceId());

if ($idpMetadata === null) {
throw new MetadataNotFoundException(sprintf(
'No metadata found for remote provider with SHA1 ID: %s',
$artifact->getSourceId(),
));
}

$b->setIdpMetadata($idpMetadata);
$b->setSPMetadata($this->spMetadata);
}

$response = $b->receive($request);
Assert::isInstanceOf($response, Response::class, ResourceNotRecognizedException::class);

// Validate the signature (if any)
$this->ValidatedResponse = $this->validateResponseSignature($response);

// Validate that the destination matches the appropriate endpoint from the SP-metadata
$this->validateResponseDestination($b);

// Validate that the status is 'success'
$this->validateResponseStatus();

// TODO: validate the assertion
// TODO: create a new Response-object with the validated Assertion in it and return it to the implementation
}


/**
* Validate the status of the received response.
*
*/
private function validateResponseStatus(): void
{
if (!$this->validatedResponse->isSuccess()) {
throw new RemoteException($this->validatedResponse->getStatus());
}
}


/**
* Validate the destination of the received response.
*
* @param \SimpleSAML\SAML2\Binding $binding
* @throws \SimpleSAML\SAML2\Exception\DestinationMismatchException
*/
private function validateResponseDestination(Binding $b): void
{
foreach ($this->spMetadata->getAssertionConsumerService() as $assertionConsumerService) {
if ($assertionConsumerService->getLocation() === $this->validatedResponse->getDestination()) {
if (Binding::getBinding($assertionConsumerService->getBinding()) instanceof $b) {
return;
}
}
}

throw new ResourceNotRecognizedException();
}


/**
* Validate the signature of the received response.
*
* @param \SimpleSAML\SAML2\XML\samlp\Response $response
* @return \SimpleSAML\SAML2\XML\samlp\Response The validated response.
*
* @throws \SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException
*/
private function validateResponseSignature(Response $response): Response
{
// Validate the signature on the response, if any
if (!$response->isSigned()) {
return $response;
}

$factory = $this->spMetadata->getSignatureAlgorithmFactory();
$signatureAlgorithm = $response->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm();
foreach ($this->spMetadata->getValidatingKeys() as $validatingKey) {
$verifier = $factory->getAlgorithm($signatureAlgorithm, $validatingKey);

try {
return $response->verify($verifier);
} catch (SignatureVerificationFailedException $e) {
continue;
}
}

throw new SignatureVerificationFailedException();
}


/**
* Find IdP-metadata based on a SHA-1 hash of the entityID. Return `null` if not found.
*/
abstract protected function getIdPMetadataForSha1(string $sourceId): ?Metadata\IdentityProvider;


/**
* Find IdP-metadata based on an entityID. Return `null` if not found.
*/
abstract protected function getIdPMetadata(string $entityId): ?Metadata\IdentityProvider;


/**
* Find SP-metadata based on an entityID. Return `null` if not found.
*/
abstract protected function getSPMetadata(string $entityId): ?Metadata\ServiceProvider;
}
12 changes: 12 additions & 0 deletions src/SAML2/Exception/MetadataNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\Exception;

/**
* Exception to be raised when no metadata was found for a specific entityID
*/
class MetadataNotFound extends RuntimeException
{
}
30 changes: 30 additions & 0 deletions src/SAML2/Exception/RemoteException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\Exception;

use SimpleSAML\SAML2\XML\samlp\Status;

use function sprintf;

/**
* Exception to be raised when a status other than 'success' was received.
*/
class RemoteException extends RuntimeException
{
public function __construct(Status $status)
{
$statusCode = $status->getStatusCode();
$message = $statusCode->getValue();

// Until proven necessary, we go just one level deep
foreach ($statusCode->getSubCode() as $subCode) {
$message = sprintf("%s / %s", $message, $subCode->getValue());
}

$message = sprintf("%s (%s)", $message, $status->getStatusMessage()->getValue());

parent::__construct($message);
}
}
32 changes: 20 additions & 12 deletions src/SAML2/HTTPArtifact.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use function base64_decode;
use function base64_encode;
use function bin2hex;
use function hexdec;
use function bindec;
use function openssl_random_pseudo_bytes;
use function pack;
use function sha1;
Expand Down Expand Up @@ -109,6 +109,21 @@ public function send(AbstractMessage $message): ResponseInterface
}


public function receiveArtifact(ServerRequestInterface $request): Artifact
{
$query = $request->getQueryParams();
if (array_key_exists('SAMLart', $query)) {
$artifact = base64_decode($query['SAMLart'], true);
$endpointIndex = bindec(substr($artifact, 2, 2));
$sourceId = bin2hex(substr($artifact, 4, 20));

return new Artifact($artifact, $endpointIndex, $sourceId);
}

throw new Exception('Missing SAMLart parameter.');
}


/**
* Receive a SAML 2 message sent using the HTTP-Artifact binding.
*
Expand All @@ -122,27 +137,20 @@ public function send(AbstractMessage $message): ResponseInterface
*/
public function receive(ServerRequestInterface $request): AbstractMessage
{
$query = $request->getQueryParams();
if (array_key_exists('SAMLart', $query)) {
$artifact = base64_decode($query['SAMLart'], true);
$endpointIndex = bin2hex(substr($artifact, 2, 2));
$sourceId = bin2hex(substr($artifact, 4, 20));
} else {
throw new Exception('Missing SAMLart parameter.');
}
$artifact = $this->receiveArtifact($request);

/** @psalm-suppress UndefinedClass */
$metadataHandler = MetaDataStorageHandler::getMetadataHandler(Configuration::getInstance());

$idpMetadata = $metadataHandler->getMetaDataConfigForSha1($sourceId, 'saml20-idp-remote');
$idpMetadata = $metadataHandler->getMetaDataConfigForSha1($artifact->getSourceId(), 'saml20-idp-remote');

if ($idpMetadata === null) {
throw new Exception('No metadata found for remote provider with SHA1 ID: ' . var_export($sourceId, true));
throw new Exception('No metadata found for remote provider with SHA1 ID: ' . var_export($artifact->getSourceId(), true));
}

$endpoint = null;
foreach ($idpMetadata->getEndpoints('ArtifactResolutionService') as $ep) {
if ($ep['index'] === hexdec($endpointIndex)) {
if ($ep['index'] === $artifact->getEndpointIndex()) {
$endpoint = $ep;
break;
}
Expand Down
Loading

0 comments on commit 2bc4ad3

Please sign in to comment.