Skip to content

Commit

Permalink
Backport samlp:RequestedAuthnContext
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed Jul 30, 2023
1 parent e102756 commit 8ae4412
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 65 deletions.
69 changes: 11 additions & 58 deletions src/SAML2/AuthnRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use SimpleSAML\SAML2\XML\samlp\IDPEntry;
use SimpleSAML\SAML2\XML\samlp\IDPList;
use SimpleSAML\SAML2\XML\samlp\NameIDPolicy;
use SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext;
use SimpleSAML\SAML2\XML\samlp\RequesterID;
use SimpleSAML\SAML2\XML\samlp\Scoping;
use SimpleSAML\XML\Exception\MissingElementException;
Expand Down Expand Up @@ -105,13 +106,9 @@ class AuthnRequest extends Request
/**
* What authentication context was requested.
*
* Array with the following elements.
* - AuthnContextClassRef (required)
* - Comparison (optional)
*
* @var array
* @var \SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext|null
*/
private array $requestedAuthnContext = [];
private ?RequestedAuthnContext $requestedAuthnContext = null;

/**
* Audiences to send in the request.
Expand Down Expand Up @@ -178,7 +175,9 @@ public function __construct(DOMElement $xml = null)
$nameIdPolicy = NameIDPolicy::getChildrenOfClass($xml);
$this->nameIdPolicy = array_pop($nameIdPolicy);

$this->parseRequestedAuthnContext($xml);
$requestedAuthnContext = RequestedAuthnContext::getChildrenOfClass($xml);
$this->requestedAuthnContext = array_pop($requestedAuthnContext);

$this->parseConditions($xml);

$scoping = Scoping::getChildrenOfClass($xml);
Expand Down Expand Up @@ -233,42 +232,6 @@ private function parseSubject(DOMElement $xml): void
}


/**
* @param \DOMElement $xml
* @return void
*/
protected function parseRequestedAuthnContext(DOMElement $xml): void
{
$xpCache = XPath::getXPath($xml);

/** @var \DOMElement[] $requestedAuthnContext */
$requestedAuthnContext = XPath::xpQuery($xml, './saml_protocol:RequestedAuthnContext', $xpCache);
if (empty($requestedAuthnContext)) {
return;
}

$requestedAuthnContext = $requestedAuthnContext[0];

$rac = [
'AuthnContextClassRef' => [],
'Comparison' => Constants::COMPARISON_EXACT,
];

$xpCache = XPath::getXPath($requestedAuthnContext);
/** @var \DOMElement[] $accr */
$accr = XPath::xpQuery($requestedAuthnContext, './saml_assertion:AuthnContextClassRef', $xpCache);
foreach ($accr as $i) {
$rac['AuthnContextClassRef'][] = trim($i->textContent);
}

if ($requestedAuthnContext->hasAttribute('Comparison')) {
$rac['Comparison'] = $requestedAuthnContext->getAttribute('Comparison');
}

$this->requestedAuthnContext = $rac;
}


/**
* @param \DOMElement $xml
* @return void
Expand Down Expand Up @@ -532,9 +495,9 @@ public function setAssertionConsumerServiceIndex(int $assertionConsumerServiceIn
/**
* Retrieve the RequestedAuthnContext.
*
* @return array|null The RequestedAuthnContext.
* @return \SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext|null The RequestedAuthnContext.
*/
public function getRequestedAuthnContext(): ?array
public function getRequestedAuthnContext(): ?RequestedAuthnContext
{
return $this->requestedAuthnContext;
}
Expand All @@ -543,10 +506,10 @@ public function getRequestedAuthnContext(): ?array
/**
* Set the RequestedAuthnContext.
*
* @param array|null $requestedAuthnContext The RequestedAuthnContext.
* @param \SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext|null $requestedAuthnContext The RequestedAuthnContext.
* @return void
*/
public function setRequestedAuthnContext(array $requestedAuthnContext = null): void
public function setRequestedAuthnContext(?RequestedAuthnContext $requestedAuthnContext = null): void
{
$this->requestedAuthnContext = $requestedAuthnContext;
}
Expand Down Expand Up @@ -709,17 +672,7 @@ public function toUnsignedXML(): DOMElement

$this->addConditions($root);

$rac = $this->requestedAuthnContext;
if (!empty($rac) && !empty($rac['AuthnContextClassRef'])) {
$e = $this->document->createElementNS(Constants::NS_SAMLP, 'RequestedAuthnContext');
$root->appendChild($e);
if (isset($rac['Comparison']) && $rac['Comparison'] !== Constants::COMPARISON_EXACT) {
$e->setAttribute('Comparison', $rac['Comparison']);
}
foreach ($rac['AuthnContextClassRef'] as $accr) {
XMLUtils::addString($e, Constants::NS_SAML, 'AuthnContextClassRef', $accr);
}
}
$this->requestedAuthnContext?->toXML($root);

if ($this->scoping !== null) {
$this->scoping->toXML($root);
Expand Down
125 changes: 125 additions & 0 deletions src/SAML2/XML/samlp/RequestedAuthnContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\SAML2\XML\samlp;

use DOMElement;
use SimpleSAML\Assert\Assert;
use SimpleSAML\SAML2\Utils;
use SimpleSAML\SAML2\XML\saml\AuthnContextClassRef;
use SimpleSAML\SAML2\XML\saml\AuthnContextDeclRef;
use SimpleSAML\XML\Constants as C;
use SimpleSAML\XML\Exception\InvalidDOMElementException;

use function array_merge;

/**
* Class representing SAML2 RequestedAuthnContext
*
* @package simplesamlphp/saml2
*/
final class RequestedAuthnContext extends AbstractSamlpElement
{
/**
* Initialize a RequestedAuthnContext.
*
* @param (
* \SimpleSAML\SAML2\XML\saml\AuthnContextClassRef|
* \SimpleSAML\SAML2\XML\saml\AuthnContextDeclRef
* )[] $requestedAuthnContexts
* @param string $Comparison
*/
public function __construct(
protected array $requestedAuthnContexts = [],
protected ?string $Comparison = null,
) {
Assert::maxCount($requestedAuthnContexts, C::UNBOUNDED_LIMIT);
Assert::minCount($requestedAuthnContexts, 1);
Assert::allIsInstanceOfAny($requestedAuthnContexts, [AuthnContextClassRef::class, AuthnContextDeclRef::class]);

if ($requestedAuthnContexts[0] instanceof AuthnContextClassRef) {
Assert::allIsInstanceOf(
$requestedAuthnContexts,
AuthnContextClassRef::class,
'You need either AuthnContextClassRef or AuthnContextDeclRef, not both.',
);
} else { // Can only be AuthnContextDeclRef
Assert::allIsInstanceOf(
$requestedAuthnContexts,
AuthnContextDeclRef::class,
'You need either AuthnContextClassRef or AuthnContextDeclRef, not both.',
);
}
Assert::nullOrOneOf($Comparison, ['exact', 'minimum', 'maximum', 'better']);
}


/**
* Collect the value of the requestedAuthnContexts-property
*
* @return (\SimpleSAML\SAML2\XML\saml\AuthnContextClassRef|\SimpleSAML\SAML2\XML\saml\AuthnContextDeclRef)[]
*/
public function getRequestedAuthnContexts(): array
{
return $this->requestedAuthnContexts;
}


/**
* Collect the value of the Comparison-property
*
* @return string|null
*/
public function getComparison(): ?string
{
return $this->Comparison;
}


/**
* Convert XML into a RequestedAuthnContext
*
* @param \DOMElement $xml The XML element we should load
* @return static
*
* @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
* if the qualified name of the supplied element is wrong
*/
public static function fromXML(DOMElement $xml): static
{
Assert::same($xml->localName, 'RequestedAuthnContext', InvalidDOMElementException::class);
Assert::same($xml->namespaceURI, RequestedAuthnContext::NS, InvalidDOMElementException::class);

return new static(
array_merge(
AuthnContextClassRef::getChildrenOfClass($xml),
AuthnContextDeclRef::getChildrenOfClass($xml),
),
self::getOptionalAttribute($xml, 'Comparison', null),
);
}


/**
* Convert this RequestedAuthnContext to XML.
*
* @param \DOMElement|null $parent The element we should append this RequestedAuthnContext to.
* @return \DOMElement
*/
public function toXML(DOMElement $parent = null): DOMElement
{
/** @psalm-var \DOMDocument $e->ownerDocument */
$e = $this->instantiateParentElement($parent);

foreach ($this->getRequestedAuthnContexts() as $context) {
$context->toXML($e);
}

if ($this->getComparison() !== null) {
$e->setAttribute('Comparison', $this->getComparison());
}

return $e;
}
}
18 changes: 11 additions & 7 deletions tests/SAML2/AuthnRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
use SimpleSAML\SAML2\AuthnRequest;
use SimpleSAML\SAML2\Constants as C;
use SimpleSAML\SAML2\XML\saml\Audience;
use SimpleSAML\SAML2\XML\saml\AuthnContextClassRef;
use SimpleSAML\SAML2\XML\saml\Issuer;
use SimpleSAML\SAML2\XML\saml\NameID;
use SimpleSAML\SAML2\XML\samlp\IDPEntry;
use SimpleSAML\SAML2\XML\samlp\IDPList;
use SimpleSAML\SAML2\XML\samlp\NameIDPolicy;
use SimpleSAML\SAML2\XML\samlp\RequestedAuthnContext;
use SimpleSAML\SAML2\XML\samlp\RequesterID;
use SimpleSAML\SAML2\XML\samlp\Scoping;
use SimpleSAML\SAML2\Utils\XPath;
Expand All @@ -32,13 +34,15 @@ class AuthnRequestTest extends TestCase
public function testUnmarshalling(): void
{
$authnRequest = new AuthnRequest();
$authnRequest->setRequestedAuthnContext([
'AuthnContextClassRef' => [
'accr1',
'accr2',
],
'Comparison' => 'better',
]);
$authnRequest->setRequestedAuthnContext(
new RequestedAuthnContext(
[
new AuthnContextClassRef('accr1'),
new AuthnContextClassRef('accr2'),
],
'better',
),
);

$authnRequestElement = $authnRequest->toUnsignedXML();

Expand Down
Loading

0 comments on commit 8ae4412

Please sign in to comment.