/*
 * Copyright 2004,2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.rahas.impl;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axis2.context.MessageContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.rahas.*;
import org.apache.rahas.impl.util.*;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.util.DateUtil;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.signature.XMLSignature;
import org.opensaml.core.xml.XMLObjectBuilder;
import org.opensaml.saml.common.SAMLObjectBuilder;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AttributeValue;
import org.opensaml.saml.saml2.core.AuthnContext;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.KeyInfoConfirmationDataType;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;

import org.opensaml.xmlsec.signature.X509Data;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.Signer;

import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.core.xml.schema.impl.XSStringBuilder;
import org.opensaml.xmlsec.signature.Signature;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
 * WS-Trust based SAML2 token issuer. This issuer will generate request security token responses with SAML2
 * assertions.
 */
public class SAML2TokenIssuer implements TokenIssuer {

    private String configParamName;

    private OMElement configElement;

    private String configFile;

    protected List<Signature> signatureList = new ArrayList<Signature>();

    private boolean isSymmetricKeyBasedHoK = false;

    private SAMLTokenIssuerConfig tokenIssuerConfiguration;

    private static Log log = LogFactory.getLog(SAML2TokenIssuer.class);

    /**
     * This is the main method which issues SAML2 assertions as security token responses. This method will
     * read issuer configuration and in message context properties (Basically request security token properties)
     * and will create a security token response with SAML2 assertion. The attributes are retrieved from a callback
     * class.
     * @param data A populated <code>RahasData</code> instance
     * @return A SOAP message with security token response (as per ws-trust spec) with a SAML2 assertion.
     * @throws TrustException If an error occurred while creating the response.
     */
    public SOAPEnvelope issue(RahasData data) throws TrustException {
        MessageContext inMsgCtx = data.getInMessageContext();

        this.tokenIssuerConfiguration = CommonUtil.getTokenIssuerConfiguration(this.configElement,
                this.configFile, inMsgCtx.getParameter(this.configParamName));

        if (tokenIssuerConfiguration == null) {

            if (log.isDebugEnabled()) {
                String parameterName;
                if (this.configElement != null) {
                    parameterName = "OMElement - " + this.configElement.toString();
                } else if (this.configFile != null) {
                    parameterName = "File - " + this.configFile;
                } else if (this.configParamName != null) {
                    parameterName = "With message context parameter name - " + this.configParamName;
                } else {
                    parameterName = "No method to build configurations";
                }

                log.debug("Unable to build token configurations, " + parameterName);
            }

            throw new TrustException("configurationIsNull");
        }

        SOAPEnvelope env = TrustUtil.createSOAPEnvelope(inMsgCtx
                .getEnvelope().getNamespace().getNamespaceURI());

        Crypto crypto = tokenIssuerConfiguration.getIssuerCrypto(inMsgCtx
                .getAxisService().getClassLoader());

        // Get the document
        Document doc = ((Element) env).getOwnerDocument();

        // Get the key size and create a new byte array of that size
        int keySize = data.getKeySize();
        keySize = (keySize == -1) ? tokenIssuerConfiguration.getKeySize() : keySize;

        data.setKeySize(keySize);

        // Build the assertion
        Assertion assertion = buildAssertion(doc, crypto, data);

        // Sign the assertion
        Assertion signedAssertion = signAssertion(doc, assertion, crypto);

        return createRequestSecurityTokenResponse(data, signedAssertion, env);

    }

    /**
     * This method prepares the final response. This method will create a request security token response as
     * specified in WS-Trust specification. The equivalent XML would take following format,
     * <pre>  &lt;wst:RequestSecurityTokenResponse xmlns:wst="..."&gt;
     *       &lt;wst:TokenType&gt;...&lt;/wst:TokenType&gt;
     *       &lt;wst:RequestedSecurityToken&gt;...&lt;/wst:RequestedSecurityToken&gt;
     *       ...
     *       &lt;wsp:AppliesTo xmlns:wsp="..."&gt;...&lt;/wsp:AppliesTo&gt;
     *       &lt;wst:RequestedAttachedReference&gt;
     *       ...
     *       &lt;/wst:RequestedAttachedReference&gt;
     *       &lt;wst:RequestedUnattachedReference&gt;
     *       ...
     *       &lt;/wst:RequestedUnattachedReference&gt;
     *       &lt;wst:RequestedProofToken&gt;...&lt;/wst:RequestedProofToken&gt;
     *       &lt;wst:Entropy&gt;
     *       &lt;wst:BinarySecret&gt;...&lt;/wst:BinarySecret&gt;
     *       &lt;/wst:Entropy&gt;
     *       &lt;wst:Lifetime&gt;...&lt;/wst:Lifetime&gt;
     *   &lt;/wst:RequestSecurityTokenResponse&gt;</pre>
     *
     *   Thus the RequestedSecurityToken will have SAML2 assertion passed.
     * @param rahasData The configuration data which comes with RST
     * @param assertion OpenSAM representation of SAML2 assertion.
     * @param soapEnvelope SOAP message envelope
     * @return SOAPEnvelope which includes RequestSecurityTokenResponse
     * @throws TrustException If an error occurred while creating RequestSecurityTokenResponse.
     */
    protected SOAPEnvelope createRequestSecurityTokenResponse(RahasData rahasData,
                                                      Assertion assertion,
                                                      SOAPEnvelope soapEnvelope) throws TrustException {

        OMElement requestSecurityTokenResponse;
        int wstVersion = rahasData.getVersion();
        if (RahasConstants.VERSION_05_02 == wstVersion) {
            requestSecurityTokenResponse = TrustUtil.createRequestSecurityTokenResponseElement(
                    wstVersion, soapEnvelope.getBody());
        } else {
            OMElement requestSecurityTokenResponseCollectionElement = TrustUtil
                    .createRequestSecurityTokenResponseCollectionElement(
                            wstVersion, soapEnvelope.getBody());
            requestSecurityTokenResponse = TrustUtil.createRequestSecurityTokenResponseElement(
                    wstVersion, requestSecurityTokenResponseCollectionElement);
        }

        TrustUtil.createTokenTypeElement(wstVersion, requestSecurityTokenResponse).setText(
                RahasConstants.TOK_TYPE_SAML_20);

        if (rahasData.getKeyType().endsWith(RahasConstants.KEY_TYPE_SYMM_KEY)) {
            TrustUtil.createKeySizeElement(wstVersion, requestSecurityTokenResponse, rahasData.getKeySize());
        }

        if (tokenIssuerConfiguration.isAddRequestedAttachedRef()) {
            TrustUtil.createRequestedAttachedRef(wstVersion, requestSecurityTokenResponse, "#"
                    + assertion.getID(), RahasConstants.TOK_TYPE_SAML_20);
        }

        if (tokenIssuerConfiguration.isAddRequestedUnattachedRef()) {
            TrustUtil.createRequestedUnattachedRef(wstVersion, requestSecurityTokenResponse,
                    assertion.getID(), RahasConstants.TOK_TYPE_SAML_20);
        }

        if (rahasData.getAppliesToAddress() != null) {
            TrustUtil.createAppliesToElement(requestSecurityTokenResponse, rahasData
                    .getAppliesToAddress(), rahasData.getAddressingNs());
        }

        // Use GMT time in milliseconds
        ZonedDateTime creationTime = ZonedDateTime.ofInstant(rahasData.getAssertionCreatedDate().toInstant(), ZoneOffset.UTC);
        ZonedDateTime expirationTime = ZonedDateTime.ofInstant(rahasData.getAssertionExpiringDate().toInstant(), ZoneOffset.UTC);

        // Add the Lifetime element
        TrustUtil.createLifetimeElement(wstVersion, requestSecurityTokenResponse, DateUtil.getDateTimeFormatter(true).format(creationTime), DateUtil.getDateTimeFormatter(true).format(expirationTime));

        // Create the RequestedSecurityToken element and add the SAML token
        // to it
        OMElement requestedSecurityTokenElement = TrustUtil
                .createRequestedSecurityTokenElement(wstVersion, requestSecurityTokenResponse);

        Element assertionElement = assertion.getDOM();

        requestedSecurityTokenElement.addChild((OMNode)assertionElement);

        // Store the token
        Token assertionToken = new Token(assertion.getID(),
                (OMElement) assertionElement, rahasData.getAssertionCreatedDate(),
                rahasData.getAssertionExpiringDate());

        // At this point we definitely have the secret
        // Otherwise it should fail with an exception earlier
        assertionToken.setSecret(rahasData.getEphmeralKey());
        TrustUtil.getTokenStore(rahasData.getInMessageContext()).add(assertionToken);

        if (rahasData.getKeyType().endsWith(RahasConstants.KEY_TYPE_SYMM_KEY)
                && tokenIssuerConfiguration.getKeyComputation()
                != SAMLTokenIssuerConfig.KeyComputation.KEY_COMP_USE_REQ_ENT) {

            Document doc = ((Element) soapEnvelope).getOwnerDocument();

            // Add the RequestedProofToken
            TokenIssuerUtil.handleRequestedProofToken(rahasData, wstVersion,
                    tokenIssuerConfiguration,
                    requestSecurityTokenResponse, assertionToken, doc);
        }

        return soapEnvelope;

    }

    /**
     * This methods builds the SAML2 assertion. The equivalent XML would look as follows,
     * <pre>&lt;saml:Assertion
     *      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="b07b804c-7c29-ea16-7300-4f3d6f7928ac"
     *      Version="2.0"
     *      IssueInstant="2004-12-05T09:22:05Z"&gt;
     *      &lt;saml:Issuer&gt;https://idp.example.org/SAML2&lt;/saml:Issuer&gt;
     *      &lt;ds:Signature
     *        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt;...&lt;/ds:Signature&gt;
     *      &lt;saml:Subject&gt;
     *        &lt;saml:NameID
     *          Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"&gt;
     *          3f7b3dcf-1674-4ecd-92c8-1544f346baf8
     *        &lt;/saml:NameID&gt;
     *        &lt;saml:SubjectConfirmation
     *          Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"&gt;
     *          &lt;saml:SubjectConfirmationData
     *            InResponseTo="aaf23196-1773-2113-474a-fe114412ab72"
     *            Recipient="https://sp.example.com/SAML2/SSO/POST"
     *            NotOnOrAfter="2004-12-05T09:27:05Z"/&gt;
     *        &lt;/saml:SubjectConfirmation&gt;
     *      &lt;/saml:Subject&gt;
     *      &lt;saml:Conditions
     *        NotBefore="2004-12-05T09:17:05Z"
     *        NotOnOrAfter="2004-12-05T09:27:05Z"&gt;
     *        &lt;saml:AudienceRestriction&gt;
     *          &lt;saml:Audience&gt;https://sp.example.com/SAML2&lt;/saml:Audience&gt;
     *        &lt;/saml:AudienceRestriction&gt;
     *      &lt;/saml:Conditions&gt;
     *      &lt;saml:AuthnStatement
     *        AuthnInstant="2004-12-05T09:22:00Z"
     *        SessionIndex="b07b804c-7c29-ea16-7300-4f3d6f7928ac"&gt;
     *        &lt;saml:AuthnContext&gt;
     *          &lt;saml:AuthnContextClassRef&gt;
     *            urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
     *         &lt;/saml:AuthnContextClassRef&gt;
     *        &lt;/saml:AuthnContext&gt;
     *      &lt;/saml:AuthnStatement&gt;
     *      &lt;saml:AttributeStatement&gt;
     *        &lt;saml:Attribute
     *          xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
     *          x500:Encoding="LDAP"
     *          NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
     *          Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
     *          FriendlyName="eduPersonAffiliation"&gt;
     *          &lt;saml:AttributeValue
     *            xsi:type="xs:string"&gt;member&lt;/saml:AttributeValue&gt;
     *          &lt;saml:AttributeValue
     *            xsi:type="xs:string"&gt;staff&lt;/saml:AttributeValue&gt;
     *        &lt;/saml:Attribute&gt;
     *      &lt;/saml:AttributeStatement&gt;
     *    &lt;/saml:Assertion&gt;</pre>
     *
     *    Reference - en.wikipedia.org/wiki/SAML_2.0#SAML_2.0_Assertions
     * @param doc The Document which comprises SAML 2 assertion.
     * @param crypto Crypto properties.
     * @param data The RST data and other configuration information.
     * @return OpenSAML representation of an Assertion.
     * @throws TrustException If an error occurred while creating the Assertion.
     */
    protected Assertion buildAssertion(Document doc, Crypto crypto, RahasData data) throws TrustException {
        //Build the assertion
        Assertion assertion = SAML2Utils.createAssertion();

        Issuer issuer = SAML2Utils.createIssuer(this.tokenIssuerConfiguration.getIssuerName());
        assertion.setIssuer(issuer);

        // Validity period
        Instant creationDate = Instant.now();
        Instant expirationDate = Instant.ofEpochMilli(creationDate.toEpochMilli() + tokenIssuerConfiguration.getTtl());

        data.setAssertionCreatedDate(Date.from(creationDate));
        data.setAssertionExpiringDate(Date.from(expirationDate));

        // Set the issued time.
        assertion.setIssueInstant(creationDate);

        // These variables are used to build the trust assertion
        Conditions conditions = SAML2Utils.createConditions(creationDate, expirationDate);
        assertion.setConditions(conditions);

        // Create the subject
        Subject subject;

        if (!data.getKeyType().endsWith(RahasConstants.KEY_TYPE_BEARER)) {
            subject = createSubjectWithHolderOfKeySubjectConfirmation(doc, crypto,
                    creationDate, expirationDate, data);
        } else {
            subject = createSubjectWithBearerSubjectConfirmation(data);
        }

        // Set the subject
        assertion.setSubject(subject);

        // If a SymmetricKey is used build an attr stmt, if a public key is build an authn stmt.
        if (isSymmetricKeyBasedHoK) {
            AttributeStatement attrStmt = createAttributeStatement(data);
            assertion.getAttributeStatements().add(attrStmt);
        } else {
            AuthnStatement authStmt = createAuthenticationStatement(data);
            assertion.getAuthnStatements().add(authStmt);
            if (data.getClaimDialect() != null && data.getClaimElem() != null) {
                assertion.getAttributeStatements().add(createAttributeStatement(data));
            }
        }

        return assertion;
    }

    /**
     * This method will create a SAML 2 subject based on Holder of Key confirmation method.
     * The relevant XML would look as follows,
     * <pre>  &lt;saml2:Subject&gt;
     *       &lt;saml2:NameID&gt;
     *           ...
     *       &lt;/saml2:NameID&gt;
     *       &lt;saml2:SubjectConfirmation
     *               Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"&gt;
     *           &lt;saml2:SubjectConfirmationData
     *                   xsi:type="saml2:KeyInfoConfirmationDataType"&gt;
     *               &lt;ds:KeyInfo&gt;
     *                   &lt;ds:KeyValue&gt;...&lt;/ds:KeyValue&gt;
     *               &lt;/ds:KeyInfo&gt;
     *           &lt;/saml2:SubjectConfirmationData&gt;
     *       &lt;/saml2:SubjectConfirmation&gt;
     *   &lt;/saml2:Subject&gt;</pre>
     *
     * KeyInfo can be created based on public key or symmetric key. That is decided by looking at
     * the RahasData.getKeyType. TODO make sure this implementation is correct.
     * Theoretically we should be able to have many subject confirmation methods in a SAML2 subject.
     * TODO - Do we need to support that ?
     * @param doc The original XML document which we need to include the assertion.
     * @param crypto The relevant crypto properties
     * @param creationTime The time that assertion was created.
     * @param expirationTime The expiring time
     * @param data The configuration data relevant request.
     * @return OpenSAML representation of the SAML2 object.
     * @throws TrustException If an error occurred while creating the subject.
     */
    protected Subject createSubjectWithHolderOfKeySubjectConfirmation(Document doc, Crypto crypto,
                                                            Instant creationTime,
                                                            Instant expirationTime, RahasData data)
            throws TrustException {


        // Create the subject
        Subject subject = (Subject)CommonUtil.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);

        // Set the subject name identifier
        if (data.getPrincipal() != null) {
            setSubjectNamedIdentifierData(subject, data.getPrincipal().getName(), NameID.EMAIL);
        }

        // Create KeyInfo
        KeyInfo keyInfo = createKeyInfo(doc, crypto, data);

        //Build the Subject Confirmation
        SubjectConfirmation subjectConfirmation
                = (SubjectConfirmation)CommonUtil.buildXMLObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME);

        //Set the subject Confirmation method
        subjectConfirmation.setMethod("urn:oasis:names:tc:SAML:2.0:cm:holder-of-key");

        //Build the subject confirmation data element
        KeyInfoConfirmationDataType scData = createKeyInfoConfirmationDataType();

        //Set the keyInfo element
        scData.getKeyInfos().add(keyInfo);

        // Set the validity period
        scData.setNotBefore(creationTime);
        scData.setNotOnOrAfter(expirationTime);

        //Set the subject confirmation data
        subjectConfirmation.setSubjectConfirmationData(scData);

        //set the subject confirmation
        subject.getSubjectConfirmations().add(subjectConfirmation);

        log.debug("SAML2.0 subject is constructed successfully.");
        return subject;
    }

    private KeyInfoConfirmationDataType createKeyInfoConfirmationDataType() {
        SAMLObjectBuilder<KeyInfoConfirmationDataType> keyInfoSubjectConfirmationDataBuilder = (SAMLObjectBuilder<KeyInfoConfirmationDataType>) XMLObjectProviderRegistrySupport.getBuilderFactory().<KeyInfoConfirmationDataType>getBuilderOrThrow(KeyInfoConfirmationDataType.TYPE_NAME);

        //Build the subject confirmation data element
        return keyInfoSubjectConfirmationDataBuilder.
                buildObject(SubjectConfirmationData.DEFAULT_ELEMENT_NAME, KeyInfoConfirmationDataType.TYPE_NAME);
    }

    /**
     * This method creates a subject element with the bearer subject confirmation method.
     * <pre>   &lt;saml:Subject&gt;
     *       &lt;saml:NameIdentifier
     *                   NameQualifier="www.example.com"
     *                   Format="urn:oasis:names:tc:SAML:1.1:nameid-
     *           format:X509SubjectName"&gt;
     *           uid=joe,ou=people,ou=saml-demo,o=baltimore.com
     *       &lt;/saml:NameIdentifier&gt;
     *       &lt;saml:SubjectConfirmation&gt;
     *           &lt;saml:ConfirmationMethod&gt;
     *               urn:oasis:names:tc:SAML:1.0:cm:bearer
     *           &lt;/saml:ConfirmationMethod&gt;
     *       &lt;/saml:SubjectConfirmation&gt;
     *   &lt;/saml:Subject&gt;</pre>
     * @param data RahasData element
     * @return  SAML 2.0 Subject element with Bearer subject confirmation
     * @throws org.apache.rahas.TrustException if an error occurred while creating the subject.
     */
    protected Subject createSubjectWithBearerSubjectConfirmation(RahasData data) throws TrustException {

        // Create the subject
        Subject subject = (Subject)CommonUtil.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);

        //Create NameID and attach it to the subject
        setSubjectNamedIdentifierData(subject, data.getPrincipal().getName(), NameID.EMAIL);

        //Build the Subject Confirmation
        SubjectConfirmation subjectConfirmation
                = (SubjectConfirmation)CommonUtil.buildXMLObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME);

        //Set the subject Confirmation method
        subjectConfirmation.setMethod("urn:oasis:names:tc:SAML:2.0:cm:bearer");

        subject.getSubjectConfirmations().add(subjectConfirmation);
        return subject;
    }


    /**
     * This method signs the given assertion with issuer's private key.
     * @param document The original RST document.
     * @param assertion Assertion to be signed.
     * @param crypto  The cryptographic properties.
     * @return The signed assertion.
     * @throws TrustException If an error occurred while signing the assertion.
     */
    protected Assertion signAssertion(Document document, Assertion assertion, Crypto crypto) throws TrustException {

        // Create a SignKeyHolder to hold the crypto objects that are used to sign the assertion
        SignKeyHolder signKeyHolder = createSignKeyHolder(crypto);

        // Build the signature object and set the credentials.
        Signature signature = (Signature) CommonUtil.buildXMLObject(Signature.DEFAULT_ELEMENT_NAME);

        signature.setSigningCredential(signKeyHolder);
        signature.setSignatureAlgorithm(signKeyHolder.getSignatureAlgorithm());
        signature.setCanonicalizationAlgorithm(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

        //Build the KeyInfo element and set the certificate
        try {
            KeyInfo keyInfo = (KeyInfo) CommonUtil.buildXMLObject(KeyInfo.DEFAULT_ELEMENT_NAME);
            X509Data x509Data = (X509Data) CommonUtil.buildXMLObject(X509Data.DEFAULT_ELEMENT_NAME);
            org.opensaml.xmlsec.signature.X509Certificate cert
                    = (org.opensaml.xmlsec.signature.X509Certificate) CommonUtil.buildXMLObject
                    (org.opensaml.xmlsec.signature.X509Certificate.DEFAULT_ELEMENT_NAME);
            String value
                    = org.apache.xml.security.utils.Base64.encode(signKeyHolder.getEntityCertificate().getEncoded());

            cert.setValue(value);
            x509Data.getX509Certificates().add(cert);
            keyInfo.getX509Datas().add(x509Data);

            signature.setKeyInfo(keyInfo);
            assertion.setSignature(signature);

            signatureList.add(signature);

            //Marshall and Sign
            Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(assertion);
            marshaller.marshall(assertion,document);

            Signer.signObjects(signatureList);

        } catch (CertificateEncodingException e) {
            throw new TrustException("Error in setting the signature", e);
        } catch (SignatureException e) {
            throw new TrustException("errorSigningAssertion", e);
        } catch (MarshallingException e) {
            throw new TrustException("errorMarshallingAssertion", e);
        }

        log.debug("SAML2.0 assertion is marshalled and signed..");

        return assertion;
    }

    /**
     * This method is used to create SignKeyHolder instances that contains the credentials required for signing the
     * assertion
     * @param crypto The crypto properties associated with the issuer.
     * @return  SignKeyHolder object.
     * @throws TrustException  If an error occurred while creating SignKeyHolder object.
     */
    private SignKeyHolder createSignKeyHolder(Crypto crypto) throws TrustException {

        SignKeyHolder signKeyHolder = new SignKeyHolder();

        try {
            X509Certificate[] issuerCerts = CommonUtil.getCertificatesByAlias(crypto,
                    this.tokenIssuerConfiguration.getIssuerKeyAlias());

            String sigAlgo = XMLSignature.ALGO_ID_SIGNATURE_RSA;
            String pubKeyAlgo = issuerCerts[0].getPublicKey().getAlgorithm();
            if (pubKeyAlgo.equalsIgnoreCase("DSA")) {
                sigAlgo = XMLSignature.ALGO_ID_SIGNATURE_DSA;
            }

            java.security.Key issuerPK = crypto.getPrivateKey(
                    this.tokenIssuerConfiguration.getIssuerKeyAlias(),
                    this.tokenIssuerConfiguration.getIssuerKeyPassword());

            signKeyHolder.setIssuerCerts(issuerCerts);
            signKeyHolder.setIssuerPK((PrivateKey) issuerPK);
            signKeyHolder.setSignatureAlgorithm(sigAlgo);

        } catch (Exception e) {
            throw new TrustException("Error creating issuer signature");
        }

        log.debug("SignKeyHolder object is created with the credentials..");

        return signKeyHolder;
    }

    /**
     * This method creates an AttributeStatement. The relevant XML would look like as follows,
     * <pre>  &lt;saml:AttributeStatement&gt;
     *    &lt;saml:Attribute
     *      xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
     *      x500:Encoding="LDAP"
     *      NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
     *      Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
     *      FriendlyName="eduPersonAffiliation"&gt;
     *      &lt;saml:AttributeValue
     *        xsi:type="xs:string"&gt;member&lt;/saml:AttributeValue&gt;
     *      &lt;saml:AttributeValue
     *        xsi:type="xs:string"&gt;staff&lt;/saml:AttributeValue&gt;
     *    &lt;/saml:Attribute&gt;
     *  &lt;/saml:AttributeStatement&gt;</pre>
     *  Reference -  http://en.wikipedia.org/wiki/SAML_2.0#SAML_2.0_Assertions
     * @param data The RahasData which carry information about RST.
     * @return An AttributeStatement with filled attributes retrieved by calling callback class.
     * @throws TrustException If an error occurred while creating the AttributeStatement.
     */
    protected AttributeStatement createAttributeStatement(RahasData data) throws TrustException {


        AttributeStatement attributeStatement
                = (AttributeStatement) CommonUtil.buildXMLObject(AttributeStatement.DEFAULT_ELEMENT_NAME);

        Attribute[] attributes;

        SAMLCallbackHandler handler = CommonUtil.getSAMLCallbackHandler(this.tokenIssuerConfiguration, data);

        SAMLAttributeCallback cb = new SAMLAttributeCallback(data);
        if (handler != null) {
            try {
                handler.handle(cb);
            } catch (Exception e) {
                throw new TrustException(
                            "errorCallingSAMLCallback",
                            e);
            }

            attributes = cb.getSAML2Attributes();
        } else { //else add the attribute with a default value

            log.debug("No callback registered to get attributes ... Using default attributes");

            // TODO do we need to remove this ?
            Attribute attribute = (Attribute) CommonUtil.buildXMLObject(Attribute.DEFAULT_ELEMENT_NAME);
            attribute.setName("Name");
            attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified");

            XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();

            XSStringBuilder attributeValueBuilder = (XSStringBuilder) builderFactory
                    .getBuilder(XSString.TYPE_NAME);

            XSString stringValue = attributeValueBuilder.buildObject(
                    AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);

            attributeValueBuilder.buildObject(
                    AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
            stringValue.setValue("Colombo/Rahas");
            attribute.getAttributeValues().add(stringValue);
            attributes = new Attribute[1];
            attributes[0] = attribute;
        }
        //add attributes to the attribute statement
        attributeStatement.getAttributes().addAll(Arrays.asList(attributes));

        log.debug("SAML2.0 attribute statement is constructed successfully.");

        return attributeStatement;
    }

    /**
     * This method creates an authentication statement. The equivalent XML would look as follows,
     * <pre>  &lt;saml:AuthnStatement
     *    AuthnInstant="2004-12-05T09:22:00Z"
     *    SessionIndex="b07b804c-7c29-ea16-7300-4f3d6f7928ac"&gt;
     *    &lt;saml:AuthnContext&gt;
     *      &lt;saml:AuthnContextClassRef&gt;
     *        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
     *     &lt;/saml:AuthnContextClassRef&gt;
     *    &lt;/saml:AuthnContext&gt;
     *  &lt;/saml:AuthnStatement&gt;</pre>
     * @param data The RahasData which carry information about RST.
     * @return OpenSAML representation of an AuthnStatement class.
     * @throws TrustException If an error occurred while creating the authentication statement.
     */
    protected AuthnStatement createAuthenticationStatement(RahasData data) throws TrustException {

        MessageContext inMsgCtx = data.getInMessageContext();

        //build the auth stmt
        AuthnStatement authenticationStatement
                = (AuthnStatement)CommonUtil.buildXMLObject(AuthnStatement.DEFAULT_ELEMENT_NAME);

        // set the authn instance
        // TODO do we need to use the same time as specified in the conditions ?
        authenticationStatement.setAuthnInstant(Instant.now());

        // Create authentication context
        AuthnContext authContext = (AuthnContext)CommonUtil.buildXMLObject(AuthnContext.DEFAULT_ELEMENT_NAME);

        // Create authentication context class reference
        AuthnContextClassRef authCtxClassRef
                = (AuthnContextClassRef)CommonUtil.buildXMLObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
        
        //if username/password based authn
        if (inMsgCtx.getProperty(RahasConstants.USERNAME) != null) {
            authCtxClassRef.setAuthnContextClassRef(AuthnContext.PASSWORD_AUTHN_CTX);
        } else if (inMsgCtx.getProperty(RahasConstants.X509_CERT) != null) { //if X.509 cert based authn
            authCtxClassRef.setAuthnContextClassRef(AuthnContext.X509_AUTHN_CTX);
        }

        authContext.setAuthnContextClassRef(authCtxClassRef);
        authenticationStatement.setAuthnContext(authContext);

        log.debug("SAML2.0 authentication statement is constructed successfully.");

        return authenticationStatement;
    }

    /**
     * This method will set the subject principal details to the given subject.
     * @param subject The subject.
     * @param subjectNameId Subject name id, to identify the principal
     * @param format Format of the subjectNameId, i.e. email, x509subject etc ...
     * @throws TrustException If an error occurred while building NameID.
     */
    protected static void setSubjectNamedIdentifierData(Subject subject, String subjectNameId, String format)
            throws TrustException {

        //Create NameID and attach it to the subject
        NameID nameID = SAML2Utils.createNamedIdentifier(subjectNameId, format);
        subject.setNameID(nameID);
    }

    /**
     * This method creates the KeyInfo relevant for the assertion. The KeyInfo could be created in 2 ways.
     * 1. Using symmetric key - KeyInfo is created using a symmetric key
     * 2. Using a public key - KeyInfo created using a public key
     * The methodology is decided by looking at RahasData.getKeyType() method.
     * @param doc The document which we are processing.
     * @param crypto Includes crypto properties relevant to issuer.
     * @param data Includes metadata about the RST.
     * @return OpenSAML representation of KeyInfo.
     * @throws TrustException If an error occurred while creating the KeyInfo object.
     */
    protected KeyInfo createKeyInfo(Document doc, Crypto crypto, RahasData data)
            throws TrustException {

        KeyInfo keyInfo;

        // If it is a Symmetric Key
        if (data.getKeyType().endsWith(RahasConstants.KEY_TYPE_SYMM_KEY)) {

            isSymmetricKeyBasedHoK = true;
            X509Certificate serviceCert = null;
            try {

                // Get AppliesTo to figure out which service to issue the token
                // for
                serviceCert = this.tokenIssuerConfiguration.getServiceCert(crypto, data.getAppliesToAddress());

                keyInfo = CommonUtil.getSymmetricKeyBasedKeyInfo(doc, data, serviceCert, data.getKeySize(), crypto,
                        tokenIssuerConfiguration.getKeyComputation());

            } catch (Exception e) {
                if (serviceCert != null) {
                    throw new TrustException(
                            "errorInBuildingTheEncryptedKeyForPrincipal",
                            new String[]{serviceCert.getSubjectDN().getName()},
                            e);
                } else {
                    throw new TrustException(
                            "errorInBuildingTheEncryptedKeyForPrincipal",
                            new String[]{"UnknownSubjectDN"},
                            e);
                }
            }

        } else if (data.getKeyType().endsWith(RahasConstants.KEY_TYPE_PUBLIC_KEY)) {    // If it is a public Key

            try {
                // Create the ds:KeyValue element with the ds:X509Data
                X509Certificate clientCert = data.getClientCert();

                if (clientCert == null) {
                    // TODO are we always looking up by alias ? Dont we need to lookup by any other attribute ?
                    clientCert = CommonUtil.getCertificateByAlias(crypto, data.getPrincipal().getName());
                }

                keyInfo = CommonUtil.getCertificateBasedKeyInfo(clientCert);

            } catch (Exception e) {
                throw new TrustException("samlAssertionCreationError", e);
            }
        } else {
            log.error("Unidentified key type " + data.getKeyType());
            throw new TrustException(
                            "unidentifiedKeyType",
                            new String[]{data.getKeyType()});
        }

        return keyInfo;

    }

    public String getResponseAction(RahasData data) throws TrustException {
        return null;
    }

    public void setConfigurationFile(String configFile) {
        this.configFile = configFile;
    }

    public void setConfigurationElement(OMElement configElement) {
        this.configElement = configElement;
    }

    public void setConfigurationParamName(String configParamName) {
        this.configParamName = configParamName;
    }

}
