View source
<?php
namespace Drupal\miniorange_saml;
use DOMElement;
use DOMText;
class SAML2_Assertion {
private $id;
private $issueInstant;
private $issuer;
private $nameId;
private $encryptedNameId;
private $encryptedAttribute;
private $encryptionKey;
private $notBefore;
private $notOnOrAfter;
private $validAudiences;
private $sessionNotOnOrAfter;
private $sessionIndex;
private $authnInstant;
private $authnContextClassRef;
private $authnContextDecl;
private $authnContextDeclRef;
private $AuthenticatingAuthority;
private $attributes;
private $nameFormat;
private $signatureKey;
private $certificates;
private $signatureData;
private $requiredEncAttributes;
private $SubjectConfirmation;
protected $wasSignedAtConstruction = FALSE;
public function __construct(DOMElement $xml = NULL) {
$this->id = Utilities::generateId();
$this->issueInstant = Utilities::generateTimestamp();
$this->issuer = '';
$this->authnInstant = Utilities::generateTimestamp();
$this->attributes = array();
$this->nameFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified';
$this->certificates = array();
$this->AuthenticatingAuthority = array();
$this->SubjectConfirmation = array();
if ($xml === NULL) {
return;
}
if ($xml->localName === 'EncryptedAssertion') {
Utilities::ShowErrorMessage('Encrypted Assertion received.', 'Encrypted Assertion Feature is available in Premium and Enterprise Plugins only, please upgrade to use this Feature.', 'You could have enabled "Encrypted Assertion" at the Identity Provider Application setup. ');
}
if (!$xml
->hasAttribute('ID')) {
throw new \Exception('Missing ID attribute on SAML assertion.');
}
$this->id = $xml
->getAttribute('ID');
if ($xml
->getAttribute('Version') !== '2.0') {
throw new \Exception('Unsupported version: ' . $xml
->getAttribute('Version'));
}
$this->issueInstant = Utilities::xsDateTimeToTimestamp($xml
->getAttribute('IssueInstant'));
$issuer = Utilities::xpQuery($xml, './saml_assertion:Issuer');
if (empty($issuer)) {
throw new \Exception('Missing <saml:Issuer> in assertion.');
}
$this->issuer = trim($issuer[0]->textContent);
$this
->parseConditions($xml);
$this
->parseAuthnStatement($xml);
$this
->parseAttributes($xml);
$this
->parseEncryptedAttributes($xml);
$this
->parseSignature($xml);
$this
->parseSubject($xml);
}
private function parseSubject(DOMElement $xml) {
$subject = Utilities::xpQuery($xml, './saml_assertion:Subject');
if (empty($subject)) {
return;
}
elseif (count($subject) > 1) {
throw new \Exception('More than one <saml:Subject> in <saml:Assertion>.');
}
$subject = $subject[0];
$nameId = Utilities::xpQuery($subject, './saml_assertion:NameID | ./saml_assertion:EncryptedID/xenc:EncryptedData');
if (empty($nameId)) {
throw new \Exception('Missing <saml:NameID> or <saml:EncryptedID> in <saml:Subject>.');
}
elseif (count($nameId) > 1) {
throw new \Exception('More than one <saml:NameID> or <saml:EncryptedD> in <saml:Subject>.');
}
$nameId = $nameId[0];
if ($nameId->localName === 'EncryptedData') {
$this->encryptedNameId = $nameId;
}
else {
$this->nameId = Utilities::parseNameId($nameId);
}
}
private function parseConditions(DOMElement $xml) {
$conditions = Utilities::xpQuery($xml, './saml_assertion:Conditions');
if (empty($conditions)) {
return;
}
elseif (count($conditions) > 1) {
throw new \Exception('More than one <saml:Conditions> in <saml:Assertion>.');
}
$conditions = $conditions[0];
if ($conditions
->hasAttribute('NotBefore')) {
$notBefore = Utilities::xsDateTimeToTimestamp($conditions
->getAttribute('NotBefore'));
if ($this->notBefore === NULL || $this->notBefore < $notBefore) {
$this->notBefore = $notBefore;
}
}
if ($conditions
->hasAttribute('NotOnOrAfter')) {
$notOnOrAfter = Utilities::xsDateTimeToTimestamp($conditions
->getAttribute('NotOnOrAfter'));
if ($this->notOnOrAfter === NULL || $this->notOnOrAfter > $notOnOrAfter) {
$this->notOnOrAfter = $notOnOrAfter;
}
}
for ($node = $conditions->firstChild; $node !== NULL; $node = $node->nextSibling) {
if ($node instanceof DOMText) {
continue;
}
if ($node->namespaceURI !== 'urn:oasis:names:tc:SAML:2.0:assertion') {
throw new \Exception('Unknown namespace of condition: ' . var_export($node->namespaceURI, TRUE));
}
switch ($node->localName) {
case 'AudienceRestriction':
$audiences = Utilities::extractStrings($node, 'urn:oasis:names:tc:SAML:2.0:assertion', 'Audience');
if ($this->validAudiences === NULL) {
$this->validAudiences = $audiences;
}
else {
$this->validAudiences = array_intersect($this->validAudiences, $audiences);
}
break;
case 'OneTimeUse':
break;
case 'ProxyRestriction':
break;
default:
throw new \Exception('Unknown condition: ' . var_export($node->localName, TRUE));
}
}
}
private function parseAuthnStatement(DOMElement $xml) {
$authnStatements = Utilities::xpQuery($xml, './saml_assertion:AuthnStatement');
if (empty($authnStatements)) {
$this->authnInstant = NULL;
return;
}
elseif (count($authnStatements) > 1) {
throw new \Exception('More that one <saml:AuthnStatement> in <saml:Assertion> not supported.');
}
$authnStatement = $authnStatements[0];
if (!$authnStatement
->hasAttribute('AuthnInstant')) {
throw new \Exception('Missing required AuthnInstant attribute on <saml:AuthnStatement>.');
}
$this->authnInstant = Utilities::xsDateTimeToTimestamp($authnStatement
->getAttribute('AuthnInstant'));
if ($authnStatement
->hasAttribute('SessionNotOnOrAfter')) {
$this->sessionNotOnOrAfter = Utilities::xsDateTimeToTimestamp($authnStatement
->getAttribute('SessionNotOnOrAfter'));
}
if ($authnStatement
->hasAttribute('SessionIndex')) {
$this->sessionIndex = $authnStatement
->getAttribute('SessionIndex');
}
$this
->parseAuthnContext($authnStatement);
}
private function parseAuthnContext(DOMElement $authnStatementEl) {
$authnContexts = Utilities::xpQuery($authnStatementEl, './saml_assertion:AuthnContext');
if (count($authnContexts) > 1) {
throw new \Exception('More than one <saml:AuthnContext> in <saml:AuthnStatement>.');
}
elseif (empty($authnContexts)) {
throw new \Exception('Missing required <saml:AuthnContext> in <saml:AuthnStatement>.');
}
$authnContextEl = $authnContexts[0];
$authnContextDeclRefs = Utilities::xpQuery($authnContextEl, './saml_assertion:AuthnContextDeclRef');
if (count($authnContextDeclRefs) > 1) {
throw new \Exception('More than one <saml:AuthnContextDeclRef> found?');
}
elseif (count($authnContextDeclRefs) === 1) {
$this
->setAuthnContextDeclRef(trim($authnContextDeclRefs[0]->textContent));
}
$authnContextDecls = Utilities::xpQuery($authnContextEl, './saml_assertion:AuthnContextDecl');
if (count($authnContextDecls) > 1) {
throw new \Exception('More than one <saml:AuthnContextDecl> found?');
}
elseif (count($authnContextDecls) === 1) {
$this
->setAuthnContextDecl(new SAML2_XML_Chunk($authnContextDecls[0]));
}
$authnContextClassRefs = Utilities::xpQuery($authnContextEl, './saml_assertion:AuthnContextClassRef');
if (count($authnContextClassRefs) > 1) {
throw new \Exception('More than one <saml:AuthnContextClassRef> in <saml:AuthnContext>.');
}
elseif (count($authnContextClassRefs) === 1) {
$this
->setAuthnContextClassRef(trim($authnContextClassRefs[0]->textContent));
}
if (empty($this->authnContextClassRef) && empty($this->authnContextDecl) && empty($this->authnContextDeclRef)) {
throw new \Exception('Missing either <saml:AuthnContextClassRef> or <saml:AuthnContextDeclRef> or <saml:AuthnContextDecl>');
}
$this->AuthenticatingAuthority = Utilities::extractStrings($authnContextEl, 'urn:oasis:names:tc:SAML:2.0:assertion', 'AuthenticatingAuthority');
}
private function parseAttributes(DOMElement $xml) {
$firstAttribute = TRUE;
$attributes = Utilities::xpQuery($xml, './saml_assertion:AttributeStatement/saml_assertion:Attribute');
foreach ($attributes as $attribute) {
if (!$attribute
->hasAttribute('Name')) {
throw new Exception('Missing name on <saml:Attribute> element.');
}
$name = $attribute
->getAttribute('Name');
if ($attribute
->hasAttribute('NameFormat')) {
$nameFormat = $attribute
->getAttribute('NameFormat');
}
else {
$nameFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified';
}
if ($firstAttribute) {
$this->nameFormat = $nameFormat;
$firstAttribute = FALSE;
}
else {
if ($this->nameFormat !== $nameFormat) {
$this->nameFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified';
}
}
if (!array_key_exists($name, $this->attributes)) {
$this->attributes[$name] = array();
}
$values = Utilities::xpQuery($attribute, './saml_assertion:AttributeValue');
foreach ($values as $value) {
$this->attributes[$name][] = trim($value->textContent);
}
}
}
private function parseEncryptedAttributes(DOMElement $xml) {
$this->encryptedAttribute = Utilities::xpQuery($xml, './saml_assertion:AttributeStatement/saml_assertion:EncryptedAttribute');
}
private function parseSignature(DOMElement $xml) {
$sig = Utilities::validateElement($xml);
if ($sig !== FALSE) {
$this->wasSignedAtConstruction = TRUE;
$this->certificates = $sig['Certificates'];
$this->signatureData = $sig;
}
}
public function validate(XMLSecurityKey $key) {
if ($this->signatureData === NULL) {
return FALSE;
}
Utilities::validateSignature($this->signatureData, $key);
return TRUE;
}
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getIssueInstant() {
return $this->issueInstant;
}
public function setIssueInstant($issueInstant) {
$this->issueInstant = $issueInstant;
}
public function getIssuer() {
return $this->issuer;
}
public function setIssuer($issuer) {
$this->issuer = $issuer;
}
public function getNameId() {
if ($this->encryptedNameId !== NULL) {
throw new \Exception('Attempted to retrieve encrypted NameID without decrypting it first.');
}
return $this->nameId;
}
public function setNameId($nameId) {
$this->nameId = $nameId;
}
public function isNameIdEncrypted() {
if ($this->encryptedNameId !== NULL) {
return TRUE;
}
return FALSE;
}
public function encryptNameId(XMLSecurityKey $key) {
$doc = new DOMDocument();
$root = $doc
->createElement('root');
$doc
->appendChild($root);
Utilities::addNameId($root, $this->nameId);
$nameId = $root->firstChild;
Utilities::getContainer()
->debugMessage($nameId, 'encrypt');
$enc = new XMLSecEnc();
$enc
->setNode($nameId);
$enc->type = XMLSecEnc::Element;
$symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC);
$symmetricKey
->generateSessionKey();
$enc
->encryptKey($key, $symmetricKey);
$this->encryptedNameId = $enc
->encryptNode($symmetricKey);
$this->nameId = NULL;
}
public function getValidAudiences() {
return $this->validAudiences;
}
public function getSessionIndex() {
return $this->sessionIndex;
}
public function setAuthnContext($authnContext) {
$this
->setAuthnContextClassRef($authnContext);
}
public function setAuthnContextClassRef($authnContextClassRef) {
$this->authnContextClassRef = $authnContextClassRef;
}
public function setAuthnContextDecl(SAML2_XML_Chunk $authnContextDecl) {
if (!empty($this->authnContextDeclRef)) {
throw new Exception('AuthnContextDeclRef is already registered! May only have either a Decl or a DeclRef, not both!');
}
$this->authnContextDecl = $authnContextDecl;
}
public function setAuthnContextDeclRef($authnContextDeclRef) {
if (!empty($this->authnContextDecl)) {
throw new Exception('AuthnContextDecl is already registered! May only have either a Decl or a DeclRef, not both!');
}
$this->authnContextDeclRef = $authnContextDeclRef;
}
public function getAttributes() {
return $this->attributes;
}
public function setAttributes(array $attributes) {
$this->attributes = $attributes;
}
public function getSignatureData() {
return $this->signatureData;
}
public function toXML(DOMNode $parentElement = NULL) {
if ($parentElement === NULL) {
$document = new DOMDocument();
$parentElement = $document;
}
else {
$document = $parentElement->ownerDocument;
}
$root = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:' . 'Assertion');
$parentElement
->appendChild($root);
$root
->setAttributeNS('urn:oasis:names:tc:SAML:2.0:protocol', 'samlp:tmp', 'tmp');
$root
->removeAttributeNS('urn:oasis:names:tc:SAML:2.0:protocol', 'tmp');
$root
->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:tmp', 'tmp');
$root
->removeAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'tmp');
$root
->setAttributeNS('http://www.w3.org/2001/XMLSchema', 'xs:tmp', 'tmp');
$root
->removeAttributeNS('http://www.w3.org/2001/XMLSchema', 'tmp');
$root
->setAttribute('ID', $this->id);
$root
->setAttribute('Version', '2.0');
$root
->setAttribute('IssueInstant', gmdate('Y-m-d\\TH:i:s\\Z', $this->issueInstant));
$issuer = Utilities::addString($root, 'urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Issuer', $this->issuer);
$this
->addSubject($root);
$this
->addConditions($root);
$this
->addAuthnStatement($root);
if ($this->requiredEncAttributes == FALSE) {
$this
->addAttributeStatement($root);
}
else {
$this
->addEncryptedAttributeStatement($root);
}
if ($this->signatureKey !== NULL) {
Utilities::insertSignature($this->signatureKey, $this->certificates, $root, $issuer->nextSibling);
}
return $root;
}
private function addSubject(DOMElement $root) {
if ($this->nameId === NULL && $this->encryptedNameId === NULL) {
return;
}
$subject = $root->ownerDocument
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Subject');
$root
->appendChild($subject);
if ($this->encryptedNameId === NULL) {
Utilities::addNameId($subject, $this->nameId);
}
else {
$eid = $subject->ownerDocument
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:' . 'EncryptedID');
$subject
->appendChild($eid);
$eid
->appendChild($subject->ownerDocument
->importNode($this->encryptedNameId, TRUE));
}
foreach ($this->SubjectConfirmation as $sc) {
$sc
->toXML($subject);
}
}
private function addConditions(DOMElement $root) {
$document = $root->ownerDocument;
$conditions = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Conditions');
$root
->appendChild($conditions);
if ($this->notBefore !== NULL) {
$conditions
->setAttribute('NotBefore', gmdate('Y-m-d\\TH:i:s\\Z', $this->notBefore));
}
if ($this->notOnOrAfter !== NULL) {
$conditions
->setAttribute('NotOnOrAfter', gmdate('Y-m-d\\TH:i:s\\Z', $this->notOnOrAfter));
}
if ($this->validAudiences !== NULL) {
$ar = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AudienceRestriction');
$conditions
->appendChild($ar);
Utilities::addStrings($ar, 'urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Audience', FALSE, $this->validAudiences);
}
}
private function addAuthnStatement(DOMElement $root) {
if ($this->authnInstant === NULL || $this->authnContextClassRef === NULL && $this->authnContextDecl === NULL && $this->authnContextDeclRef === NULL) {
return;
}
$document = $root->ownerDocument;
$authnStatementEl = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AuthnStatement');
$root
->appendChild($authnStatementEl);
$authnStatementEl
->setAttribute('AuthnInstant', gmdate('Y-m-d\\TH:i:s\\Z', $this->authnInstant));
if ($this->sessionNotOnOrAfter !== NULL) {
$authnStatementEl
->setAttribute('SessionNotOnOrAfter', gmdate('Y-m-d\\TH:i:s\\Z', $this->sessionNotOnOrAfter));
}
if ($this->sessionIndex !== NULL) {
$authnStatementEl
->setAttribute('SessionIndex', $this->sessionIndex);
}
$authnContextEl = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AuthnContext');
$authnStatementEl
->appendChild($authnContextEl);
if (!empty($this->authnContextClassRef)) {
Utilities::addString($authnContextEl, 'urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AuthnContextClassRef', $this->authnContextClassRef);
}
if (!empty($this->authnContextDecl)) {
$this->authnContextDecl
->toXML($authnContextEl);
}
if (!empty($this->authnContextDeclRef)) {
Utilities::addString($authnContextEl, 'urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AuthnContextDeclRef', $this->authnContextDeclRef);
}
Utilities::addStrings($authnContextEl, 'urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AuthenticatingAuthority', FALSE, $this->AuthenticatingAuthority);
}
private function addAttributeStatement(DOMElement $root) {
if (empty($this->attributes)) {
return;
}
$document = $root->ownerDocument;
$attributeStatement = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeStatement');
$root
->appendChild($attributeStatement);
foreach ($this->attributes as $name => $values) {
$attribute = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute');
$attributeStatement
->appendChild($attribute);
$attribute
->setAttribute('Name', $name);
if ($this->nameFormat !== 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified') {
$attribute
->setAttribute('NameFormat', $this->nameFormat);
}
foreach ($values as $value) {
if (is_string($value)) {
$type = 'xs:string';
}
elseif (is_int($value)) {
$type = 'xs:integer';
}
else {
$type = NULL;
}
$attributeValue = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeValue');
$attribute
->appendChild($attributeValue);
if ($type !== NULL) {
$attributeValue
->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:type', $type);
}
if (is_null($value)) {
$attributeValue
->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:nil', 'true');
}
if ($value instanceof DOMNodeList) {
for ($i = 0; $i < $value->length; $i++) {
$node = $document
->importNode($value
->item($i), TRUE);
$attributeValue
->appendChild($node);
}
}
else {
$attributeValue
->appendChild($document
->createTextNode($value));
}
}
}
}
private function addEncryptedAttributeStatement(DOMElement $root) {
if ($this->requiredEncAttributes == FALSE) {
return;
}
$document = $root->ownerDocument;
$attributeStatement = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeStatement');
$root
->appendChild($attributeStatement);
foreach ($this->attributes as $name => $values) {
$document2 = new DOMDocument();
$attribute = $document2
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute');
$attribute
->setAttribute('Name', $name);
$document2
->appendChild($attribute);
if ($this->nameFormat !== 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified') {
$attribute
->setAttribute('NameFormat', $this->nameFormat);
}
foreach ($values as $value) {
if (is_string($value)) {
$type = 'xs:string';
}
elseif (is_int($value)) {
$type = 'xs:integer';
}
else {
$type = NULL;
}
$attributeValue = $document2
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeValue');
$attribute
->appendChild($attributeValue);
if ($type !== NULL) {
$attributeValue
->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:type', $type);
}
if ($value instanceof DOMNodeList) {
for ($i = 0; $i < $value->length; $i++) {
$node = $document2
->importNode($value
->item($i), TRUE);
$attributeValue
->appendChild($node);
}
}
else {
$attributeValue
->appendChild($document2
->createTextNode($value));
}
}
$EncAssert = new XMLSecEnc();
$EncAssert
->setNode($document2->documentElement);
$EncAssert->type = 'http://www.w3.org/2001/04/xmlenc#Element';
$symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES256_CBC);
$symmetricKey
->generateSessionKey();
$EncAssert
->encryptKey($this->encryptionKey, $symmetricKey);
$EncrNode = $EncAssert
->encryptNode($symmetricKey);
$EncAttribute = $document
->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:EncryptedAttribute');
$attributeStatement
->appendChild($EncAttribute);
$n = $document
->importNode($EncrNode, TRUE);
$EncAttribute
->appendChild($n);
}
}
}