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 d6d25e9
Show file tree
Hide file tree
Showing 8 changed files with 598 additions and 35 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

Check failure on line 34 in src/SAML2/Artifact.php

View workflow job for this annotation

GitHub Actions / Quality control

Syntax error, unexpected ':', expecting '(' on line 34
{
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

Check failure on line 56 in src/SAML2/Artifact.php

View workflow job for this annotation

GitHub Actions / Quality control

Syntax error, unexpected T_PUBLIC on line 56
{
return $this->sourceId;
}
}

Check failure on line 60 in src/SAML2/Artifact.php

View workflow job for this annotation

GitHub Actions / Quality control

Syntax error, unexpected '}', expecting EOF on line 60
238 changes: 238 additions & 0 deletions src/SAML2/Entity/ServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\Entity;

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

use function sprintf;

/**
* Class representing a SAML 2 Service Provider.
*
* @package simplesamlphp/saml2
*/
abstract class ServiceProvider
{
protected Metadata\IdentityProvider $idpMetadata;
protected ?Response $verifiedResponse;


/**
* @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.
*
* Upon receiving the response from the binding, the signature will be validated first.
* Once the signature checks out, the assertions are decrypted, their signatures verified
* and then any encrypted NameID's and/or attributes are decrypted.
*
* @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->getIdPMetadataForSha1($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);

// Verify the signature (if any)
$this->verifiedResponse = $response->isSigned() ? $this->verifyElementSignature($response) : $response;

if (!($b instanceof HTTPArtifact)) {
$idpMetadata = $this->getIdPMetadata($response->getEntityId());
}

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

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

/**
* See paragraph 6.2 of the SAML 2.0 core specifications for the applicable processing rules
*
* Long story short - Decrypt the assertion first, then validate it's signature
* Once the signature is verified, decrypt any BaseID, NameID or Attribute that's encrypted
*/
$unverifiedAssertions = $this->verifiedResponse->getAssertions();
foreach ($this->verifiedResponse->getAssertions() as $i => $assertion) {
// Decrypt the assertions
$decryptedAssertion = ($assertion instanceof EncryptedAssertion)
? $this->decryptElement($assertion)
: $assertion;

// Verify the signature on the assertions (if any)
$verifiedAssertion = $this->verifyElementSignature($decryptedAssertion);

// Decrypt the NameID and replace it inside the assertion's Subject
$nameID = $verifiedAssertion->getSubject()?->getIdentifier();

if ($nameID instanceof EncryptedID) {
$decryptedNameID = $this->decryptElement($nameID);
$subject = new Subject($decryptedNameID, $verifiedAssertion->getSubjectConfirmation());
} else {
$subject = $assertion->getSubject();
}
}

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


/**
* Decrypt the given element using the decryption keys provided to us.
*
* @param \SimpleSAML\XMLSecurity\XML\EncryptedElementInterface $element
* @return \SimpleSAML\XMLSecurity\EncryptableElementInterface
*
* @throws \SimpleSAML\SAML2\Exception\RuntimeException if none of the keys could be used to decrypt the element
*/
protected function decryptElement(EncryptedElementInterface $element): EncryptableElementInterface
{
$factory = $this->spMetadata->getEncryptionAlgorithmFactory();

$encryptionAlgorithm = ($factory instanceof EncryptionAlgorithmFactory)
? $element->getEncryptedData()->getEncryptionMethod()
: $element->getEncryptedKey()->getEncryptionMethod();

foreach ($this->spMetadata->getDecriptionKeys() as $decryptionKey) {
$decryptor = $factory->getAlgorithm($encryptionAlgorithm, $decryptionKey);
try {
return $element->decrypt($decryptor);
} catch (Exception $e) {
continue;
}
}

throw new RuntimeException(sprintf(
'Unable to decrypt %s with any of the available keys.',
$element::class,
));
}


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


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

throw new ResourceNotRecognizedException();
}


/**
* Verify the signature of an element using the available validation keys.
*
* @param \SimpleSAML\XMLSecurity\XML\SignedElementInterface $element
* @return \SimpleSAML\XMLSecurity\XML\SignableElementInterface The validated element.
*
* @throws \SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException
*/
protected function verifyElementSignature(SignedElementInterface $element): SignableElementInterface
{
$factory = $this->spMetadata->getSignatureAlgorithmFactory();
$signatureAlgorithm = $element->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm();

foreach ($this->spMetadata->getValidatingKeys() as $validatingKey) {
$verifier = $factory->getAlgorithm($signatureAlgorithm, $validatingKey);

try {
return $element->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);
}
}
Loading

0 comments on commit d6d25e9

Please sign in to comment.