Skip to content

Commit

Permalink
Add element samlp:AuthnQuery (#327)
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed Mar 6, 2023
1 parent e66719c commit 8a8ae10
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 0 deletions.
187 changes: 187 additions & 0 deletions src/SAML2/XML/samlp/AuthnQuery.php
@@ -0,0 +1,187 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\XML\samlp;

use DOMElement;
use SimpleSAML\Assert\Assert;
use SimpleSAML\SAML2\Exception\Protocol\RequestVersionTooHighException;
use SimpleSAML\SAML2\Exception\Protocol\RequestVersionTooLowException;
use SimpleSAML\SAML2\Exception\ProtocolViolationException;
use SimpleSAML\SAML2\XML\saml\Issuer;
use SimpleSAML\SAML2\XML\saml\Subject;
use SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext;
use SimpleSAML\XML\Exception\InvalidDOMElementException;
use SimpleSAML\XML\Exception\MissingElementException;
use SimpleSAML\XML\Exception\TooManyElementsException;
use SimpleSAML\XML\Utils as XMLUtils;
use SimpleSAML\XMLSecurity\XML\ds\Signature;

use function array_pop;
use function in_array;

/**
* Class for SAML 2 AuthnQuery query messages.
*
* @package simplesamlphp/saml2
*/
final class AuthnQuery extends AbstractSubjectQuery
{
/**
* Constructor for SAML 2 AuthnQuery.
*
* @param \SimpleSAML\SAML2\XML\saml\Subject $subject
* @param \SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext|null $requestedAuthnContext
* @param string|null $sessionIndex
* @param \SimpleSAML\SAML2\XML\saml\Issuer $issuer
* @param string|null $id
* @param string $version
* @param int $issueInstant
* @param string|null $destination
* @param string|null $consent
* @param \SimpleSAML\SAML2\XML\samlp\Extensions $extensions
*/
public function __construct(
Subject $subject,
protected ?RequestedAuthnContext $requestedAuthnContext = null,
protected ?string $sessionIndex = null,
?Issuer $issuer = null,
?string $id = null,
string $version = '2.0',
?int $issueInstant = null,
?string $destination = null,
?string $consent = null,
?Extensions $extensions = null,
) {
parent::__construct($subject, $issuer, $id, $version, $issueInstant, $destination, $consent, $extensions);
}


/**
* Retrieve RequestedAuthnContext.
*
* @return \SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext|null
*/
public function getRequestedAuthnContext(): ?RequestedAuthnContext
{
return $this->requestedAuthnContext;
}


/**
* Retrieve session index.
*
* @return string|null
*/
public function getSessionIndex(): ?string
{
return $this->sessionIndex;
}


/**
* Create a class from XML
*
* @param \DOMElement $xml
* @return static
*
* @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
* if the qualified name of the supplied element is wrong
* @throws \SimpleSAML\XML\Exception\MissingAttributeException
* if the supplied element is missing one of the mandatory attributes
* @throws \SimpleSAML\XML\Exception\MissingElementException
* if one of the mandatory child-elements is missing
* @throws \SimpleSAML\XML\Exception\TooManyElementsException
* if too many child-elements of a type are specified
*/
public static function fromXML(DOMElement $xml): static
{
Assert::same($xml->localName, 'AuthnQuery', InvalidDOMElementException::class);
Assert::same($xml->namespaceURI, AuthnQuery::NS, InvalidDOMElementException::class);

/** @psalm-var string $version */
$version = self::getAttribute($xml, 'Version');
Assert::true(version_compare('2.0', $version, '<='), RequestVersionTooLowException::class);
Assert::true(version_compare('2.0', $version, '>='), RequestVersionTooHighException::class);

$id = self::getAttribute($xml, 'ID');
$destination = self::getAttribute($xml, 'Destination', null);
$consent = self::getAttribute($xml, 'Consent', null);
$sessionIndex = self::getAttribute($xml, 'SessionIndex', null);

/** @psalm-var string $issueInstant */
$issueInstant = self::getAttribute($xml, 'IssueInstant');
// Strip sub-seconds - See paragraph 1.3.3 of SAML core specifications
$issueInstant = preg_replace('/([.][0-9]+Z)$/', 'Z', $issueInstant, 1);

Assert::validDateTimeZulu($issueInstant, ProtocolViolationException::class);
$issueInstant = XMLUtils::xsDateTimeToTimestamp($issueInstant);

$requestedAuthnContext = RequestedAuthnContext::getChildrenOfClass($xml);

$issuer = Issuer::getChildrenOfClass($xml);
Assert::countBetween($issuer, 0, 1);

$extensions = Extensions::getChildrenOfClass($xml);
Assert::maxCount(
$extensions,
1,
'Only one saml:Extensions element is allowed.',
TooManyElementsException::class,
);

$subject = Subject::getChildrenOfClass($xml);
Assert::notEmpty($subject, 'Missing subject in subject query.', MissingElementException::class);
Assert::maxCount(
$subject,
1,
'More than one <saml:Subject> in AttributeQuery',
TooManyElementsException::class,
);

$signature = Signature::getChildrenOfClass($xml);
Assert::maxCount($signature, 1, 'Only one ds:Signature element is allowed.', TooManyElementsException::class);

$request = new static(
array_pop($subject),
array_pop($requestedAuthnContext),
$sessionIndex,
array_pop($issuer),
$id,
$version,
$issueInstant,
$destination,
$consent,
array_pop($extensions),
);

if (!empty($signature)) {
$request->setSignature($signature[0]);
$request->setXML($xml);
}

return $request;
}


/**
* Convert this message to an unsigned XML document.
* This method does not sign the resulting XML document.
*
* @return \DOMElement The root element of the DOM tree
*/
protected function toUnsignedXML(?DOMElement $parent = null): DOMElement
{
$e = parent::toUnsignedXML($parent);

$sessionIndex = $this->getSessionIndex();
if ($sessionIndex !== null) {
$e->setAttribute('SessionIndex', $sessionIndex);
}

$this->getRequestedAuthnContext()?->toXML($e);

return $e;
}
}
95 changes: 95 additions & 0 deletions tests/SAML2/XML/samlp/AuthnQueryTest.php
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Test\SAML2\XML\samlp;

use DOMDocument;
use PHPUnit\Framework\TestCase;
use SimpleSAML\SAML2\Constants as C;
use SimpleSAML\SAML2\Utils\XPath;
use SimpleSAML\SAML2\XML\saml\AuthnContextDeclRef;
use SimpleSAML\SAML2\XML\saml\Issuer;
use SimpleSAML\SAML2\XML\saml\NameID;
use SimpleSAML\SAML2\XML\saml\Subject;
use SimpleSAML\SAML2\XML\samlp\AuthnQuery;
use SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext;
use SimpleSAML\Test\XML\SchemaValidationTestTrait;
use SimpleSAML\Test\XML\SerializableElementTestTrait;
use SimpleSAML\XML\DOMDocumentFactory;
use SimpleSAML\XML\Exception\MissingElementException;
use SimpleSAML\XML\Exception\MissingAttributeException;
use SimpleSAML\XML\Exception\TooManyElementsException;
use SimpleSAML\XMLSecurity\TestUtils\SignedElementTestTrait;

use function dirname;
use function strval;

/**
* Class \SimpleSAML\SAML2\XML\samlp\AuthnQueryTest
*
* @covers \SimpleSAML\SAML2\XML\samlp\AuthnQuery
* @covers \SimpleSAML\SAML2\XML\samlp\AbstractSubjectQuery
* @covers \SimpleSAML\SAML2\XML\samlp\AbstractRequest
* @covers \SimpleSAML\SAML2\XML\samlp\AbstractMessage
* @covers \SimpleSAML\SAML2\XML\samlp\AbstractSamlpElement
* @package simplesamlphp/saml2
*/
final class AuthnQueryTest extends TestCase
{
use SchemaValidationTestTrait;
use SerializableElementTestTrait;
use SignedElementTestTrait;


/**
*/
public function setup(): void
{
$this->schema = dirname(__FILE__, 5) . '/schemas/saml-schema-protocol-2.0.xsd';

$this->testedClass = AuthnQuery::class;

$this->xmlRepresentation = DOMDocumentFactory::fromFile(
dirname(__FILE__, 4) . '/resources/xml/samlp_AuthnQuery.xml',
);
}


/**
*/
public function testMarshalling(): void
{
$nameId = new NameID('urn:example:subject', null, null, C::NAMEID_UNSPECIFIED);
$authnContextDeclRef = new AuthnContextDeclRef('https://example.org/relative/path/to/document.xml');
$requestedAuthnContext = new RequestedAuthnContext([$authnContextDeclRef], 'exact');

$authnQuery = new AuthnQuery(
subject: new Subject($nameId),
requestedAuthnContext: $requestedAuthnContext,
issuer: new Issuer(
value: 'https://example.org/',
Format: C::NAMEID_ENTITY,
),
id: 'aaf23196-1773-2113-474a-fe114412ab72',
issueInstant: 1504698567,
sessionIndex: 'phpunit',
);

$this->assertEquals(
$this->xmlRepresentation->saveXML($this->xmlRepresentation->documentElement),
strval($authnQuery),
);
}


public function testUnmarshalling(): void
{
$authnQuery = AuthnQuery::fromXML($this->xmlRepresentation->documentElement);

$this->assertEquals(
$this->xmlRepresentation->saveXML($this->xmlRepresentation->documentElement),
strval($authnQuery),
);
}
}
9 changes: 9 additions & 0 deletions tests/resources/xml/samlp_AuthnQuery.xml
@@ -0,0 +1,9 @@
<samlp:AuthnQuery xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="aaf23196-1773-2113-474a-fe114412ab72" IssueInstant="2017-09-06T11:49:27Z" SessionIndex="phpunit">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://example.org/</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">urn:example:subject</saml:NameID>
</saml:Subject>
<samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact">
<saml:AuthnContextDeclRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://example.org/relative/path/to/document.xml</saml:AuthnContextDeclRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnQuery>

0 comments on commit 8a8ae10

Please sign in to comment.