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 4912572
Show file tree
Hide file tree
Showing 10 changed files with 748 additions and 43 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;
}
}
325 changes: 325 additions & 0 deletions src/SAML2/Entity/ServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
<?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\RequestDenied;
use SimpleSAML\SAML2\Exception\Protocol\ResourceNotRecognizedException;
use SimpleSAML\SAML2\Exception\RemoteException;
use SimpleSAML\SAML2\Exception\RuntimeException;
use SimpleSAML\SAML2\Metadata;
use SimpleSAML\SAML2\StateProviderInterface;
use SimpleSAML\SAML2\XML\saml\EncryptedAssertion;
use SimpleSAML\SAML2\XML\samlp\Response;
use SimpleSAML\SAML2\Utils;

use function sprintf;
use function in_array;

/**
* Class representing a SAML 2 Service Provider.
*
* @package simplesamlphp/saml2
*/
final class AbstractServiceProvider
{
protected ?MetadataProviderInterface $metadataProvider = null;
protected ?StateProviderInterface $stateProvider = null;
protected Metadata\IdentityProvider $idpMetadata;
protected LogoutResponse $verifiedLogoutResponse;


/**
* @param bool $encryptedAssertions Whether assertions must be encrypted
* @param bool $disableScoping Whether to send the samlp:Scoping element in requests
* @param bool $enableUnsolicited Whether 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 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,
// Use with caution - will leave any form of validation up to the implementer
protected readonly bool $bypassResponseValidation = false,
) {
}


/**
* Receive a verified, and optionally 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 receiveResponse(ServerRequestInterface $request): Response
{
$b = Binding::getCurrentBinding($request);

if ($b instanceof HTTPArtifact) {
if ($this->metadataProvider === null) {
throw new RuntimeException(
"A MetadataProvider is required to use the HTTP-Artifact binding.",
);
} elseif ($this->storageProvider === null) {
throw new RuntimeException(
"A StorageProvider is required to use the HTTP-Artifact binding.",
);
}

$artifact = $b->receiveArtifact($request);
$this->idpMetadata = $this->metadataProvider->getIdPMetadataForSha1($artifact->getSourceId());

if ($this->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);
}

$rawResponse = $b->receive($request);
Assert::isInstanceOf($rawResponse, Response::class, ResourceNotRecognizedException::class); // Wrong message-type

// Will return a raw Response prior to any form of verification
if ($this->bypassResponseValidation === true) {
return $rawResponse;
}

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

$state = null;
$stateId = $response->getInResponseTo();

if (!empty($stateId)) {
// this should be a response to a request we sent earlier
try {
$state = $this->stateProvider::loadState($stateId, 'saml:sp:sso');
} catch (RuntimeException $e) {
// something went wrong,
Utils::getContainer()->getLogger()->warning(sprintf(
'Could not load state specified by InResponseTo: %s Processing response as unsolicited.',
$e->getMessage(),
));
}
}

if ($state === null && $this->enableUnsolicited === false) {
throw new RequestDenied('Unsolicited responses are denied by configuration.');
}

// check that the issuer is the one we are expecting
Assert::keyExists($state, 'ExpectedIssuer');
$issuer = $response->getIssuer()->getValue();

if ($state['ExpectedIssuer'] !== $issuer) {
throw new ResourceNotRecognizedException("Issuer doesn't match the one the AuthnRequest was sent to.");
}

if ($this->metadataProvider === null) {
throw new RuntimeException(
"A MetadataProvider is required to be able to perform token decryption.",
);
}

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

/**
* 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 = $verifiedResponse->getAssertions();
$verifiedAssertions = [];
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 = $verifiedAssertion->getSubject();
}

// Decrypt any occurrences of EncryptedAttribute and replace them inside the assertion's AttributeStatement
$statements = $this->verifiedAssertion->getStatements();
foreach ($this->verifiedAssertion->getStatements() as $j => $statement) {
if ($statement instanceof AttributeStatement) {
$attributes = $statement->getAttributes();
if ($statement->hasEncryptedAttributes()) {
foreach ($statement->getEncryptedAttributes() as $encryptedAttribute) {
$attributes[] = $this->decryptElement($encryptedAttribute);
}
}

$statements[$j] = new AttributeStatement($attributes);
}
}

$verifiedAssertions[] = new Assertion(
$verifiedAssertion->getIssuer(),
$verifiedAssertion->getIssueInstant(),
$verifiedAssertion->getID(),
$subject,
$verifiedAssertion->getConditions(),
$statements,
);
}

$decryptedResponse = new Response(
$verifiedResponse->getStatus(),
$verifiedResponse->getIssueInstant(),
$verifiedResponse->getIssuer(),
$verifiedResponse->getID(),
$verifiedResponse->getVersion(),
$verifiedResponse->getInResponseTo(),
$verifiedResponse->getDestination(),
$verifiedResponse->getConsent(),
$verifiedResponse->getExtensions(),
$verifiedAssertions,
);

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

// Validate that the issuer matches an entity we know
if (!($b instanceof HTTPArtifact)) {
$idpMetadata = $this->getIdPMetadata($decryptedResponse->getEntityId());
}

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

// Validate
}


/**
* 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.
*
* @param \SimpleSAML\SAML2\XML\samlp\Response $response
*/
protected function validateResponseStatus(Response $response): void
{
if (!$this->response->isSuccess()) {
throw new RemoteException($response->getStatus());
}
}


/**
* Validate the destination of the received response.
*
* @param \SimpleSAML\SAML2\Binding $binding
* @param \SimpleSAML\SAML2\XML\samlp\Response $response
* @throws \SimpleSAML\SAML2\Exception\DestinationMismatchException
*/
protected function validateResponseDestination(Binding $b, Response $response): void
{
foreach ($this->spMetadata->getAssertionConsumerService() as $assertionConsumerService) {
if ($assertionConsumerService->getLocation() === $response->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();
}
}
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
{
}
Loading

0 comments on commit 4912572

Please sign in to comment.