<?php

/**
 * An SAML 2.0 Authentication Response
 *
 * @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
 * @author Olav Morken, UNINETT AS
 * @package simpleSAMLphp
 * @version $Id$
 */
class SimpleSAML_XML_SAML20_AuthnResponse extends SimpleSAML_XML_AuthnResponse {

	
	const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0';
	
	const TRANSIENT 	= 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient';
	const EMAIL 		= 'urn:oasis:names:tc:SAML:2.0:nameid-format:email';

	/* Namespaces used in the XML representation of this object.
	 * TODO: Move these constants into a generic SAML2-class?
	 */
	const SAML2_ASSERT_NS = 'urn:oasis:names:tc:SAML:2.0:assertion';
	const SAML2_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:2.0:protocol';


	/**
	 * This variable contains an XML validator for this message.
	 */
	private $validator = NULL;


	/**
	 * This varaible contains the entitiyid of the IdP which issued this message.
	 */
	private $issuer = NULL;


	/**
	 * This variable contains the NameID of this subject. It is an associative array with
	 * two keys:
	 * - 'Format'  The type of the NameID.
	 * - 'value'   Tha value of the NameID.
	 *
	 * This variable will be set by the processSubject function. A exception will be thrown if the response
	 * contains two different NameIDs.
	 */
	private $nameid = NULL;


	/**
	 * This variable contains the SessionIndex, as set by a AuthnStatement element in an assertion.
	 */
	private $sessionIndex = NULL;


	/**
	 * This associative array contains the attribute we extract from the response.
	 */
	private $attributes = array();


	function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore) {
		$this->configuration = $configuration;
		$this->metadata = $metadatastore;
	}


	/* The following methods aren't used anymore. They are included because it is required by inheritance.
	 * TODO: Remove them.
	 */
	public function validate() { throw new Exception('TODO!'); }
	public function createSession() { throw new Exception('TODO!'); }
	public function getAttributes() { throw new Exception('TODO!'); }
	public function getIssuer() { throw new Exception('TODO!'); }
	public function getNameID() { throw new Exception('TODO!'); }


	/**
	 * This function runs an xPath query on this authentication response.
	 *
	 * @param $query  The query which should be run.
	 * @param $node   The node which this query is relative to. If this node is NULL (the default)
	 *                then the query will be relative to the root of the response.
	 * @return Whatever DOMXPath::query returns.
	 */
	private function doXPathQuery($query, $node = NULL) {
		assert('is_string($query)');

		$dom = $this->getDOM();
		assert('$dom instanceof DOMDocument');

		if($node === NULL) {
			$node = $dom->documentElement;
		}

		assert('$node instanceof DOMNode');

		$xPath = new DOMXpath($dom);
		$xPath->registerNamespace("saml", self::SAML2_ASSERT_NS);
		$xPath->registerNamespace("samlp", self::SAML2_PROTOCOL_NS);
		$xPath->registerNamespace("ds", 'http://www.w3.org/2000/09/xmldsig#');

		return $xPath->query($query, $node);
	}


	/**
	 * This function checks if the user has added the given id to 'saml2.relaxvalidation'
	 * in the saml2-idp-remote configuration.
	 *
	 * @param $id   The id which identifies a part of the verification which may be relaxed.
	 * @return TRUE if this id is added to the list, FALSE if not.
	 */
	private function isValidationRelaxed($id) {

		assert('is_string($id)');
		assert('$this->issuer != NULL');

		/* Get the metadata of the issuer. */
		$md = $this->metadata->getMetaData($this->issuer, 'saml20-idp-remote');

		if(!array_key_exists('saml2.relaxvalidation', $md)) {
			/* The user hasn't added a saml2.relaxvalidation option. */
			return FALSE;
		}

		$rv = $md['saml2.relaxvalidation'];
		if(!is_array($rv)) {
			throw new Exception('saml2.relaxvalidation must be an array.');
		}

		return in_array($id, $rv, TRUE);
	}


	/**
	 * This function finds the status of this response.
	 */
	public function findstatus() {

		$status = $this->doXPathQuery('/samlp:Response/samlp:Status/samlp:StatusCode')->item(0);
		if($status != NULL) {
			return $status->getAttribute('Value');
		}
		throw new Exception('Unable to determine the status of this SAML2 AuthnResponse message.: ' . $this->getXML());
	}

	/**
	 * This function finds the issuer of this response. It will first search the Response element,
	 * and if it isn't found there, it will search all Assertion elements.
	 */
	public function findIssuer() {

		/* First check the Response element. */
		$issuer = $this->doXPathQuery('/samlp:Response/saml:Issuer')->item(0);
		if($issuer !== NULL) {
			return $issuer->textContent;
		}

		/* Then we search the Assertion elements. */
		$issuers = $this->doXPathQuery('/samlp:Response/saml:Assertion/saml:Issuer');

		if($issuers->length === 0) {
			throw new Exception('Unable to determine the issuer of this SAML2 AuthnResponse message.');
		}

		/* Since all Issuer elements should be equal in this version of simpleSAMLphp, we pick
		 * the first Issuer element we find.
		 */
		return $issuers->item(0)->textContent;
	}


	/**
	* This function decrypts the Assertion in the AuthnResponse
	* It throws an exception if the encryptAssertion for the remote idp is true and
	* the assertion is not encrypted
	* To Do: handle multible assertions
	*/
	private function decryptAssertion() {

		$dom = $this->getDOM();
		$encryptedassertion = $this->doXPathQuery('/samlp:Response/saml:EncryptedAssertion')->item(0);
		$objenc = new XMLSecEnc();
		$encData = $objenc->locateEncryptedData($dom);
		if ($encData) {	
			$spmd = $this->metadata->getMetaDataCurrent('saml20-sp-hosted');
			$spid = $this->metadata->getMetaDataCurrentEntityID('saml20-sp-hosted');
			$objenc->setNode($encData);
			$objenc->type = $encData->getAttribute("Type");

			$key = NULL;
			$objKey = $objenc->locateKey($encData);
			if ($objKey) {
				if ($objKeyInfo = $objenc->locateKeyInfo($objKey)) {
					if ($objKeyInfo->isEncrypted) {
						$objencKey = $objKeyInfo->encryptedCtx;
						if (!isset($spmd['privatekey'])) {
							throw new Exception("Private key for decrypting assertion needed, but not specified for saml20-sp-hosted id: " . $spid);
						}
						$privatekey = @file_get_contents($this->configuration->getPathValue('certdir') . $spmd['privatekey']);
						if ($privatekey === FALSE) {
							throw new Exception("Private key for decrypting assertion specified but not found for saml20-sp-hosted id: " . $spid . " Filename: " . $spmd['privatekey']);
						}
						if(array_key_exists('privatekey_pass', $spmd)) {
							$objKeyInfo->passphrase = $spmd['privatekey_pass'];
						}
						$objKeyInfo->loadKey($privatekey);
						$key = $objencKey->decryptKey($objKeyInfo);
					} else {
						$idpmd = $this->metadata->getMetaData($this->issuer, 'saml20-idp-remote');
						if (!isset( $idpmd['sharedkey'])) {
							throw new Exception("Shared key for decrypting assertion needed, but not specified for saml20-idp-remote id: " . $this->issuer);
						}
						$key = $idpmd['sharedkey'];
					}
				}
			}

			if (empty($objKey) || empty($key)) {
				throw new Exception("Error loading key to handle Decryption: >" . var_export($objKey, true));
			}
			$objKey->loadkey($key);
			
			$decrypted = $objenc->decryptNode($objKey, false);
	
			$newdoc = new DOMDocument();
			$newdoc->loadXML('<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$decrypted.'</root>');
			$importEnc = $encData->ownerDocument->importNode($newdoc->documentElement->firstChild, TRUE);
			$encryptedassertion->parentNode->replaceChild($importEnc, $encryptedassertion);
		} else {
			$md = $this->metadata->getMetaData($this->issuer, 'saml20-idp-remote');
			if (isset($md['assertion.encryption']) && $md['assertion.encryption']) {
				throw new Exception('Received unencrypted assertion from [' . $this->issuer . '] contrary to its metadata attribute [assertion.encryption]: ' . $md['assertion.encryption']);
			}
		}
	}

	/**
	 * Validate the signature in the given node.
	 *
	 * The node should either be a samlp:Response node, or a saml:Assertion node.
	 * An exception will be thrown if an error occurs during validation.
	 *
	 * @param $node  The node which contains the ds:Signature element.
	 */
	private function validateSignature($node) {

		/* Get the metadata of the issuer. */
		$md = $this->metadata->getMetaData($this->issuer, 'saml20-idp-remote');

		$publickey = FALSE;
		if (isset($md['certificate'])) {
			$publickey = @file_get_contents($this->configuration->getPathValue('certdir') . $md['certificate']);
			if (!$publickey) {
				throw new Exception("Saml20-idp-remote id: " . $this-issuer . " 'certificate' set to ': " . $md['certificate'] . "', but no certificate found");			
			}
		}
		/* Validate the signature. */
		$this->validator = new SimpleSAML_XML_Validator($node, 'ID', $publickey);
		
		if (!$publickey) {
			if(array_key_exists('certFingerprint', $md)) {

				/* Get fingerprint for the certificate of the issuer. */
				$issuerFingerprint = $md['certFingerprint'];
	
				/* Validate the fingerprint. */
				$this->validator->validateFingerprint($issuerFingerprint);

			} elseif(array_key_exists('caFile', $md)) {

				/* Validation against a CA file. */
				$this->validator->validateCA($this->configuration->getPathValue('certdir') . $md['caFile']);
			} else {

				/* Misconfigured - neither publickey, certFingerprint or caFile given. */
				throw new Exception('Misconfigured saml20-idp-remote ' . $this->issuer . ':' .
					' Neither publickey, certFingerprint or caFile given.');
			}
		}
	}


	/**
	 * This function processes a Subject node. It will throw
	 * an Exception if the subject cannot be confirmed. On successful verification,
	 * the data stored about this subject will be saved.
	 */
	private function processSubject($subject) {

		/* We currently require urn:oasis:names:tc:SAML:2.0:cm:bearer subject confirmation. */
		$bearerValidated = false;

		/* Iterate over the SubjectConfirmation nodes, looking for it. */
		foreach($this->doXPathQuery('saml:SubjectConfirmation', $subject) as $subjectConfirmation) {
			$method = $subjectConfirmation->getAttributeNode('Method');
			if($method === NULL) {
				throw new Exception('SubjectConfirmation is missing the required Method attribute.');
			}
			if($method->value !== 'urn:oasis:names:tc:SAML:2.0:cm:bearer') {
				throw new Exception('Unhandled SubjectConfirmationData: ' . $method->value);
			}

			$subjectConfirmationData = $this->doXPathQuery('saml:SubjectConfirmationData', $subjectConfirmation);
			if($subjectConfirmationData === NULL) {
				throw new Exception('Bearer confirmation node without verification data.');
			}

			/* TODO: Verify this subject. */
		}


		/* We expect the subject node to contain a NameID element which identifies this subject. */
		$nameid = $this->doXPathQuery('saml:NameID', $subject)->item(0);
		if($nameid === NULL) {
			throw new Exception('Could not find the NameID node in a Subject node.');
		}

		$format = $nameid->getAttribute('Format');
		$value = $nameid->textContent;

		if($this->nameid === NULL) {
			/* We haven't saved a nameID earlier. Save it now. */
			$this->nameid = array('Format' => $format, 'value' => $value);
			return;
		}

		/* We have saved a nameID earlier. Verify that this nameID is equal. */
		if($this->nameid['Format'] !== $format || $this->nameid['value'] !== $value) {
			throw new Exception('Multiple assertions with different nameIDs is unsupported by simpleSAMLphp');
		}
	}


	/**
	 * This function processes a Conditions node. It will throw an exception if any of the conditions
	 * are invalid.
	 */
	private function processConditions($conditions) {

		/* First verify the NotBefore and NotOnOrAfter attributes if they are present. */
		$notBefore = $conditions->getAttribute("NotBefore");
		$notOnOrAfter = $conditions->getAttribute("NotOnOrAfter");
		if (! SimpleSAML_Utilities::checkDateConditions($notBefore, $notOnOrAfter)) {
			throw new Exception('Date check failed (between ' . $notBefore . ' and ' . $notOnOrAfter . ').' .
				' Check if the clocks on the SP and IdP are synchronized. Alternatively' .
				' you can get this message, when you move back in history or refresh an old page.');
		}


		if($this->doXPathQuery('Condition', $conditions)->length > 0) {
			if(!$this->isValidationRelaxed('unknowncondition')) {
				throw new Exception('A Conditions node in a SAML2 AuthnResponse contained a' .
					' Condition node. This is unsupported by simpleSAMLphp. To disable this' .
					' check, add \'unknowncondition\' to the \'saml2.relaxvalidation\' list in' .
					' \'saml2-idp-remote\'.');
			}
		}


		$spEntityId = $this->metadata->getMetaDataCurrentEntityID('saml20-sp-hosted');

		/* The specification says that every AudienceRestriction element must be valid, but only one
		 * Audience element in each AudienceRestriction element must be valid.
		 */
		foreach($this->doXPathQuery('AudienceRestriction', $conditions) as $ar) {

			$validAudience = false;
			foreach($this->doXPathQuery('Audience', $ar) as $a) {
				if($a->textContent === $spEntityId) {
					$validAudience = true;
				}
			}
			if(!$validAudience) {
				throw new Exception('Could not verify audience of SAML2 AuthnResponse.');
			}
		}

		/* We ignore OneTimeUse and ProxyRestriction conditions. */
	}


	/**
	 * This function processes a AuthnStatement node. It will throw an exception if the statement is
	 * invalid.
	 */
	private function processAuthnStatement($authnStatement) {
		/* Extract the SessionIndex. */
		$sessionIndex = $authnStatement->getAttributeNode('SessionIndex');
		if($sessionIndex !== NULL) {
			$sessionIndex = $sessionIndex->value;
			if($this->sessionIndex === NULL) {
				$this->sessionIndex = $sessionIndex;
			} elseif($this->sessionIndex !== $sessionIndex) {
				throw new Exception('Got two different session indexes in a SAML2 AuthnResponse.');
			}
		}
	}


	/**
	 * This function processes a AttributeStatement node.
	 */
	private function processAttributeStatement($attributeStatement) {

		$md = $this->metadata->getMetadata($this->issuer, 'saml20-idp-remote');
		$base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false;

		foreach($this->doXPathQuery('saml:Attribute/saml:AttributeValue', $attributeStatement) as $attribute) {

			$name = $attribute->parentNode->getAttribute('Name');
			$value = $attribute->textContent;

			if(!array_key_exists($name, $this->attributes)) {
				$this->attributes[$name] = array();
			}

			if ($base64) {
				
				if ($name != 'jpegPhoto') {				
					foreach(explode('_', $value) AS $v) {
	
							$this->attributes[$name][] = base64_decode($v);
					}

				} else {
					$this->attributes[$name][] = $value;
					file_put_contents('/tmp/image2.jpg', $value);
				}
				
			} else {
				$this->attributes[$name][] = $value;
			}
		}
	}


	/**
	 * This function processes a Assertion node. It will throw an exception if the assertion is invalid.
	 */
	private function processAssertion($assertion) {

		/* Make sure that the assertion is signed. */
		if(!$this->validator->isNodeValidated($assertion)) {
			throw new Exception('A SAML2 AuthnResponse contained an Assertion which isn\'t verified by' .
				' the signature.');
		}

		$subject = $this->doXPathQuery('saml:Subject', $assertion)->item(0);
		if($subject === NULL) {
			if(!$this->isValidationRelaxed('nosubject')) {
				throw new Exception('Could not find required Subject information in a SAML2' .
					' AuthnResponse. To disable this check, add \'nosubject\' to the' .
					' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
			}
		} else {
			$this->processSubject($subject);
		}

		$conditions = $this->doXPathQuery('saml:Conditions', $assertion)->item(0);
		if($conditions === NULL) {
			if(!$this->isValidationRelaxed('noconditions')) {
				throw new Exception('Could not find required Conditions node in a SAML2' .
					' AuthnResponse. To disable this check, add \'noconditions\' to the' .
					' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
			}
		} else {
			$this->processConditions($conditions);
		}

		$authnStatement = $this->doXPathQuery('saml:AuthnStatement', $assertion)->item(0);
		if($authnStatement === NULL) {
			if(!$this->isValidationRelaxed('noauthnstatement')) {
				throw new Exception('Could not find required AuthnStatement node in a SAML2' .
					' AuthnResponse. To disable this check, add \'noauthnstatement\' to the' .
					' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
			}
		} else {
			$this->processAuthnStatement($authnStatement);
		}

		$attributeStatement = $this->doXPathQuery('saml:AttributeStatement', $assertion)->item(0);
		if($attributeStatement === NULL) {
			if(!$this->isValidationRelaxed('noattributestatement')) {
				throw new Exception('Could not find required AttributeStatement in a SAML2' .
					' AuthnResponse. To disable this check, add \'noattributestatement\' to the' .
					' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
			}
		} else {
			$this->processAttributeStatement($attributeStatement);
		}
	}


	/**
	 * This function processes a response message and adds information from it to the
	 * current session if it is valid.
	 *
	 * An exception will be thrown on a processing error. If the status code is something
	 * else than [...]:Success, FALSE will be returned, and no futher processing will occur.
	 *
	 * @return  TRUE on success. FALSE on an error response. The SAML 2.0 status code can
	 *          be retrieved with the findstatus() function.
	 */
	public function process() {
		$status = $this->findstatus();
		if ($status == 'urn:oasis:names:tc:SAML:2.0:status:Success' ) {
			/* Find the issuer of this response. */
			$this->issuer = $this->findIssuer();

			/* Check for signature in the saml:Response-element, and validate it if present. */
			$signature = $this->doXPathQuery('/samlp:Response/ds:Signature');
			if($signature->length > 0) {
				$this->validateSignature($signature->item(0)->parentNode);
			}
	
			$this->decryptAssertion();

			/* Check for signature in the saml:Assertion-element(s), and validate it if present. */
			$signature = $this->doXPathQuery('/samlp:Response/saml:Assertion/ds:Signature');
			if($signature->length > 0) {
				$this->validateSignature($signature->item(0)->parentNode);
			}

			/* Process all assertions. */
			$assertions = $this->doXPathQuery('/samlp:Response/saml:Assertion');
			foreach($assertions as $assertion) {
				$this->processAssertion($assertion);
			}
	
			if($this->nameid === NULL) {
				throw new Exception('No nameID found in AuthnResponse.');
			}
	
			/* Update the session information */
			$session = SimpleSAML_Session::getInstance();
			$session->doLogin('saml2');
	
			$session->setAttributes($this->attributes);
			$session->setNameID($this->nameid);
			$session->setSessionIndex($this->sessionIndex);
			$session->setIdP($this->issuer);

			return TRUE;
		} else {
			/* A different status code. */
			return FALSE;
		}
	}


	/**
	 * This function retrieves the ID of the request this response is a
	 * response to. This ID is stored in the InResponseTo attribute of the
	 * top level DOM element.
	 *
	 * @return The ID of the request this response is a response to, or NULL if
	 *  we don't know.
	 */
	public function getInResponseTo() {
		$dom = $this->getDOM();
		if($dom === NULL) {
			return NULL;
		}

		assert('$dom instanceof DOMDocument');

		$xPath = new DOMXpath($dom);
		$xPath->registerNamespace('samlp', self::SAML2_PROTOCOL_NS);

		$query = 'string(/samlp:Response/@InResponseTo)';
		$result = $xPath->evaluate($query);
		if($result === '') {
			return NULL;
		}

		return $result;
	}		
			

	/**
	 * This function generates an AuthenticationResponse
	 *
	 *  @param $idpentityid   entityid of IdP
	 *  @param $spentityid    entityid of SP
	 *  @param $inresponseto  the ID of the request, that these message is an response to.
	 *  @param $nameid        the NameID of the user (an array)
	 *  @param $attributes    A two level array of multivalued attributes, where the first level
	 *   index is the attribute name.
	 *
	 *  @return AuthenticationResponse as string
	 */
	public function generate($idpentityid, $spentityid, $inresponseto, $nameid, $attributes, $status = 'Success') {
		
		/**
		 * Retrieving metadata for the two specific entity IDs.
		 */
		$idpmd 	= $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted');
		$spmd 	= $this->metadata->getMetaData($spentityid, 'saml20-sp-remote');
		
		$issuer = $idpentityid;
		$destination = $spmd['AssertionConsumerService'];
		
		/**
		 * Generating IDs and timestamps.
		 */
		$id = SimpleSAML_Utilities::generateID();
		$issueInstant = SimpleSAML_Utilities::generateTimestamp();
		$assertionExpire = SimpleSAML_Utilities::generateTimestamp(time() + 60 * 5);# 5 minutes
		$notBefore = SimpleSAML_Utilities::generateTimestamp(time() - 30);

		$assertionid = SimpleSAML_Utilities::generateID();

		$session = SimpleSAML_Session::getInstance();
		$sessionindex = $session->getSessionIndex();

		
		/**
		 * Handling attributes.
		 */
		$base64 = isset($spmd['base64attributes']) ? $spmd['base64attributes'] : false;
		$nameidformat = isset($spmd['NameIDFormat']) ? $spmd['NameIDFormat'] : 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient';
		$spnamequalifier = isset($spmd['SPNameQualifier']) ? $spmd['SPNameQualifier'] : $spmd['entityid'];
		
		// Attribute Name Format handling. Priority is 1) SP metadata 2) IdP metadata 3) default setting
		$attributeNameFormat = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic';
		if (isset($spmd['AttributeNameFormat']))
			$attributeNameFormat = $spmd['AttributeNameFormat'];
		elseif (isset($idpmd['AttributeNameFormat']))
			$attributeNameFormat = $idpmd['AttributeNameFormat'];

		
		$encodedattributes = '';
		foreach ($attributes AS $name => $values) {
			$encodedattributes .= self::enc_attribute($name, $values, $base64, $attributeNameFormat);
		}
		$attributestatement = '<saml:AttributeStatement>' . $encodedattributes . '</saml:AttributeStatement>';
		
		$sendattributes = isset($spmd['simplesaml.attributes']) ? $spmd['simplesaml.attributes'] : true;
		
		if (!$sendattributes) 
			$attributestatement = '';
		
		
		
		/**
		 * Handling NameID
		 */
		$nameid = null;
		if ($nameidformat == self::EMAIL) {
			$nameid = $this->generateNameID($nameidformat, $attributes[$spmd['simplesaml.nameidattribute']][0], $spnamequalifier);
		} else {
			$nameid = $this->generateNameID($nameidformat, SimpleSAML_Utilities::generateID(), $spnamequalifier);
		}

		$assertion = "";
		if ($status === 'Success') {
			$assertion = '<saml:Assertion Version="2.0"
		ID="' . $assertionid . '" IssueInstant="' . $issueInstant . '">
		<saml:Issuer>' . htmlspecialchars($issuer) . '</saml:Issuer>
		<saml:Subject>
			' . $nameid . ' 
			<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
				<saml:SubjectConfirmationData NotOnOrAfter="' . $assertionExpire . '"
					InResponseTo="' . htmlspecialchars($inresponseto). '"
					Recipient="' . htmlspecialchars($destination) . '"/>
			</saml:SubjectConfirmation>
		</saml:Subject>
		<saml:Conditions NotBefore="' . $notBefore. '" NotOnOrAfter="' . $assertionExpire. '">
            <saml:AudienceRestriction>
                <saml:Audience>' . htmlspecialchars($spentityid) . '</saml:Audience>
            </saml:AudienceRestriction>
		</saml:Conditions> 
		<saml:AuthnStatement AuthnInstant="' . $issueInstant . '"
			SessionIndex="' . htmlspecialchars($sessionindex) . '">
			<saml:AuthnContext>
				<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
			</saml:AuthnContext>
        </saml:AuthnStatement>
        ' . $attributestatement. '
    </saml:Assertion>';
		}
		
		
		/**
		 * Generating the response.
		 */
		$authnResponse = '<samlp:Response 
			xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" 
			xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" 
			xmlns:xs="http://www.w3.org/2001/XMLSchema"
			xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
			ID="' . $id . '"
			InResponseTo="' . htmlspecialchars($inresponseto) . '" Version="2.0"
			IssueInstant="' . $issueInstant . '"
			Destination="' . htmlspecialchars($destination) . '">
			<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' . htmlspecialchars($issuer) . '</saml:Issuer>
			<samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
				<samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
				Value="urn:oasis:names:tc:SAML:2.0:status:' . $status . '" />
			</samlp:Status>'
			. $assertion . 
			'</samlp:Response>';

		return $authnResponse;
	}


	private function generateNameID($type = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', 
			$value = 'anonymous', $spnamequalifier = null) {
		
		$spnamequalifiertext = '';
		if (!empty($spnamequalifier)) {
			$spnamequalifiertext = ' SPNameQualifier="' . htmlspecialchars($spnamequalifier) . '"';
		}
		
		if ($type == self::EMAIL) {
			return '<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"' . 
				$spnamequalifiertext . '>' . htmlspecialchars($value) . '</saml:NameID>';

		} else {
			return '<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"' . 
				$spnamequalifiertext. '>' . htmlspecialchars($value). '</saml:NameID>';
		}
		
	}

	
	/**
	 * This function converts an array of attribute values into an
	 * encoded saml:Attribute element which should go into the
	 * AuthnResponse. The data can optionally be base64 encoded.
	 *
	 *  @param $name      Name of this attribute.
	 *  @param $values    Array with the values of this attribute.
	 *  @param $base64    Enable base64 encoding of attribute values.
	 *  @param $attributeNameFormat		Which attribute name format to use. (See SAML 2.0 Spec for details)
	 *
	 *  @return String containing the encoded saml:attribute value for this
	 *  attribute.
	 */
	private static function enc_attribute($name, $values, $base64 = false, $attributeNameFormat) {
		assert(is_array($values));

		// Default: urn:oasis:names:tc:SAML:2.0:attrname-format:basic
		$ret = '<saml:Attribute NameFormat="' . htmlspecialchars($attributeNameFormat) . '"  Name="' . htmlspecialchars($name) . '">';

		foreach($values as $value) {
			if($base64) {
				$text = base64_encode($value);
			} else {
				$text = htmlspecialchars($value);
			}
			
			$xsiType = '';
			if ($attributeNameFormat == 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic')
				$xsiType = ' xsi:type="xs:string"';
			

			$ret .= '<saml:AttributeValue' . $xsiType . '>' . $text . '</saml:AttributeValue>';
		}

		$ret .= '</saml:Attribute>';

		return $ret;
	}
	
	
}

?>