/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.cxf.ws.security.trust;

import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.wsdl.Definition;
import javax.wsdl.Types;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.schema.Schema;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.dom.DOMSource;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import org.apache.cxf.Bus;
import org.apache.cxf.BusException;
import org.apache.cxf.binding.soap.SoapBindingConstants;
import org.apache.cxf.binding.soap.model.SoapOperationInfo;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.ModCountCopyOnWriteArrayList;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.configuration.Configurable;
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.databinding.source.SourceDataBinding;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.endpoint.ClientImpl;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.endpoint.EndpointException;
import org.apache.cxf.endpoint.EndpointImpl;
import org.apache.cxf.feature.Feature;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.InterceptorProvider;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.message.Attachment;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.rt.security.claims.ClaimCollection;
import org.apache.cxf.rt.security.utils.SecurityUtils;
import org.apache.cxf.service.Service;
import org.apache.cxf.service.model.BindingInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.service.model.MessagePartInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.cxf.staxutils.W3CDOMStreamWriter;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
import org.apache.cxf.ws.addressing.EndpointReferenceUtils;
import org.apache.cxf.ws.addressing.VersionTransformer;
import org.apache.cxf.ws.mex.MetadataExchange;
import org.apache.cxf.ws.mex.model._2004_09.Metadata;
import org.apache.cxf.ws.mex.model._2004_09.MetadataSection;
import org.apache.cxf.ws.policy.EffectivePolicy;
import org.apache.cxf.ws.policy.PolicyBuilder;
import org.apache.cxf.ws.policy.PolicyConstants;
import org.apache.cxf.ws.policy.PolicyEngine;
import org.apache.cxf.ws.policy.attachment.reference.ReferenceResolver;
import org.apache.cxf.ws.policy.attachment.reference.RemoteReferenceResolver;
import org.apache.cxf.ws.policy.builder.primitive.PrimitiveAssertion;
import org.apache.cxf.ws.security.SecurityConstants;
import org.apache.cxf.ws.security.tokenstore.SecurityToken;
import org.apache.cxf.ws.security.trust.claims.ClaimsCallback;
import org.apache.cxf.ws.security.trust.delegation.DelegationCallback;
import org.apache.cxf.ws.security.wss4j.WSS4JUtils;
import org.apache.cxf.wsdl.WSDLManager;
import org.apache.cxf.wsdl11.WSDLServiceFactory;
import org.apache.neethi.All;
import org.apache.neethi.ExactlyOne;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyComponent;
import org.apache.neethi.PolicyRegistry;
import org.apache.wss4j.common.WSS4JConstants;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoFactory;
import org.apache.wss4j.common.crypto.CryptoType;
import org.apache.wss4j.common.crypto.PasswordEncryptor;
import org.apache.wss4j.common.derivedKey.P_SHA1;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.token.Reference;
import org.apache.wss4j.common.util.DateUtil;
import org.apache.wss4j.common.util.XMLUtils;
import org.apache.wss4j.dom.WSDocInfo;
import org.apache.wss4j.dom.engine.WSSConfig;
import org.apache.wss4j.dom.engine.WSSecurityEngineResult;
import org.apache.wss4j.dom.handler.RequestData;
import org.apache.wss4j.dom.processor.EncryptedKeyProcessor;
import org.apache.wss4j.dom.util.WSSecurityUtil;
import org.apache.wss4j.dom.util.X509Util;
import org.apache.wss4j.policy.SPConstants;
import org.apache.wss4j.policy.SPConstants.SPVersion;
import org.apache.wss4j.policy.model.AbstractBinding;
import org.apache.wss4j.policy.model.AlgorithmSuite;
import org.apache.wss4j.policy.model.AlgorithmSuite.AlgorithmSuiteType;
import org.apache.wss4j.policy.model.Header;
import org.apache.wss4j.policy.model.ProtectionToken;
import org.apache.wss4j.policy.model.SecureConversationToken;
import org.apache.wss4j.policy.model.SignedParts;
import org.apache.wss4j.policy.model.Trust10;
import org.apache.wss4j.policy.model.Trust13;
import org.apache.xml.security.exceptions.Base64DecodingException;
import org.apache.xml.security.keys.content.X509Data;
import org.apache.xml.security.keys.content.keyvalues.DSAKeyValue;
import org.apache.xml.security.keys.content.keyvalues.RSAKeyValue;

/**
 * An abstract class with some functionality to invoke on a SecurityTokenService (STS) via the
 * WS-Trust protocol.
 */
public abstract class AbstractSTSClient implements Configurable, InterceptorProvider {
    private static final Logger LOG = LogUtils.getL7dLogger(AbstractSTSClient.class);

    protected Bus bus;
    protected String name = "default.sts-client";
    protected Client client;
    protected String location;

    protected String wsdlLocation;
    protected QName serviceName;
    protected QName endpointName;

    protected Policy policy;
    protected String soapVersion = SoapBindingConstants.SOAP11_BINDING_ID;
    protected int keySize = 256;
    protected boolean requiresEntropy = true;
    protected Element template;
    protected Object customContent;
    protected Object claims;
    protected CallbackHandler claimsCallbackHandler;
    protected AlgorithmSuite algorithmSuite;
    protected String namespace = STSUtils.WST_NS_05_12;
    protected String addressingNamespace = "http://www.w3.org/2005/08/addressing";
    protected String wspNamespace = "http://www.w3.org/ns/ws-policy";
    protected Object onBehalfOf;
    protected boolean enableAppliesTo = true;

    protected boolean useCertificateForConfirmationKeyInfo;
    protected boolean isSecureConv;
    protected boolean isSpnego;
    protected boolean enableLifetime;
    protected int ttl = 300;
    protected boolean sendRenewing = true;
    protected boolean allowRenewing = true;
    protected boolean allowRenewingAfterExpiry;

    protected Object actAs;
    protected String tokenType;
    protected String keyType;
    protected boolean sendKeyType = true;
    protected Message message;
    protected String context;
    protected X509Certificate useKeyCertificate;

    protected Map<String, Object> ctx = new HashMap<>();

    protected List<Interceptor<? extends Message>> in = new ModCountCopyOnWriteArrayList<>();
    protected List<Interceptor<? extends Message>> out = new ModCountCopyOnWriteArrayList<>();
    protected List<Interceptor<? extends Message>> outFault = new ModCountCopyOnWriteArrayList<>();
    protected List<Interceptor<? extends Message>> inFault = new ModCountCopyOnWriteArrayList<>();
    protected List<Feature> features;

    protected TLSClientParameters tlsClientParameters;
    protected boolean allowMexMetadataSchemaLocation;

    public AbstractSTSClient(Bus b) {
        bus = b;
    }

    public String getBeanName() {
        return name;
    }

    public void setBeanName(String s) {
        name = s;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public void setMessage(Message message) {
        this.message = message;
    }

    public void setTtl(int ttl) {
        this.ttl = ttl;
    }

    public void setEnableLifetime(boolean enableLifetime) {
        this.enableLifetime = enableLifetime;
    }

    public void setSendRenewing(boolean sendRenewing) {
        this.sendRenewing = sendRenewing;
    }

    public void setTlsClientParameters(TLSClientParameters tlsClientParameters) {
        this.tlsClientParameters = tlsClientParameters;
    }

    public boolean isAllowMexMetadataSchemaLocation() {
        return allowMexMetadataSchemaLocation;
    }

    public void setAllowMexMetadataSchemaLocation(boolean allowMexMetadataSchemaLocation) {
        this.allowMexMetadataSchemaLocation = allowMexMetadataSchemaLocation;
    }

    /**
     * Sets the WS-P policy that is applied to communications between this client and the remote server
     * if no value is supplied for {@link #setWsdlLocation(String)}.
     * <p/>
     * Accepts {@link Policy} or {@link Element} as input.
     *
     * @param newPolicy the policy object
     *
     * @throws IllegalArgumentException if {@code newPolicy} is not one of the supported types.
     */
    public void setPolicy(Object newPolicy) {
        if (newPolicy instanceof Policy) {
            this.setPolicyInternal((Policy) newPolicy);
        } else if (newPolicy instanceof Element) {
            this.setPolicyInternal((Element) newPolicy);
        } else if (newPolicy instanceof String) {
            this.setPolicyInternal((String) newPolicy);
        } else {
            throw new IllegalArgumentException("Unsupported policy object.  Type must be "
                       + "org.apache.neethi.Policy or org.w3c.dom.Element.");
        }
    }

    public void setSoap12() {
        soapVersion = SoapBindingConstants.SOAP12_BINDING_ID;
    }

    public void setSoap11() {
        soapVersion = SoapBindingConstants.SOAP11_BINDING_ID;
    }

    public void setSoap11(boolean b) {
        if (b) {
            setSoap11();
        } else {
            setSoap12();
        }
    }

    public void setAddressingNamespace(String ad) {
        addressingNamespace = ad;
    }

    public void setTrust(Trust10 trust) {
        if (trust != null) {
            if (trust instanceof Trust13) {
                namespace = STSUtils.WST_NS_05_12;
            } else {
                namespace = STSUtils.WST_NS_05_02;
            }
            requiresEntropy = trust.isRequireClientEntropy();
        }
    }

    public boolean isRequiresEntropy() {
        return requiresEntropy;
    }

    public void setRequiresEntropy(boolean requiresEntropy) {
        this.requiresEntropy = requiresEntropy;
    }

    public boolean isSecureConv() {
        return isSecureConv;
    }

    public void setSecureConv(boolean secureConv) {
        this.isSecureConv = secureConv;
    }

    public boolean isSpnego() {
        return isSpnego;
    }

    public void setSpnego(boolean spnego) {
        this.isSpnego = spnego;
    }

    public boolean isAllowRenewing() {
        return allowRenewing;
    }

    public void setAllowRenewing(boolean allowRenewing) {
        this.allowRenewing = allowRenewing;
    }

    public boolean isAllowRenewingAfterExpiry() {
        return allowRenewingAfterExpiry;
    }

    public void setAllowRenewingAfterExpiry(boolean allowRenewingAfterExpiry) {
        this.allowRenewingAfterExpiry = allowRenewingAfterExpiry;
    }

    public boolean isEnableAppliesTo() {
        return enableAppliesTo;
    }

    public void setEnableAppliesTo(boolean enableAppliesTo) {
        this.enableAppliesTo = enableAppliesTo;
    }

    public String getContext() {
        return context;
    }

    public void setContext(String context) {
        this.context = context;
    }

    public void setAlgorithmSuite(AlgorithmSuite ag) {
        algorithmSuite = ag;
    }

    public Map<String, Object> getRequestContext() {
        return ctx;
    }

    public void setProperties(Map<String, Object> p) {
        ctx.putAll(p);
    }

    public Map<String, Object> getProperties() {
        return ctx;
    }

    public void setWsdlLocation(String wsdl) {
        wsdlLocation = wsdl;
    }
    public String getWsdlLocation() {
        return wsdlLocation;
    }

    public void setServiceName(String qn) {
        serviceName = QName.valueOf(qn);
    }

    public void setEndpointName(String qn) {
        endpointName = QName.valueOf(qn);
    }

    public void setServiceQName(QName qn) {
        serviceName = qn;
    }
    public QName getServiceQName() {
        return serviceName;
    }

    public void setEndpointQName(QName qn) {
        endpointName = qn;
    }
    public QName getEndpointQName() {
        return endpointName;
    }

    public void setActAs(Object actAs) {
        this.actAs = actAs;
    }

    public void setCustomContent(Object customContent) {
        this.customContent = customContent;
    }

    public void setKeySize(int i) {
        keySize = i;
    }

    public int getKeySize() {
        return keySize;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public String getTokenType() {
        return tokenType;
    }

    public void setSendKeyType(boolean sendKeyType) {
        this.sendKeyType = sendKeyType;
    }

    public void setKeyType(String keyType) {
        this.keyType = keyType;
    }

    public void setOnBehalfOf(Object onBehalfOf) {
        this.onBehalfOf = onBehalfOf;
    }

    /**
     * Indicate whether to use the signer's public X509 certificate for the subject confirmation key info
     * when creating a RequestsSecurityToken message. If the property is set to 'false', only the public key
     * value will be provided in the request. If the property is set to 'true' the complete certificate will
     * be sent in the request.
     *
     * Note: this setting is only applicable for assertions that use an asymmetric proof key
     */
    public void setUseCertificateForConfirmationKeyInfo(boolean useCertificate) {
        this.useCertificateForConfirmationKeyInfo = useCertificate;
    }

    public boolean isUseCertificateForConfirmationKeyInfo() {
        return useCertificateForConfirmationKeyInfo;
    }

    protected void setPolicyInternal(Policy newPolicy) {
        this.policy = newPolicy;
        if (algorithmSuite == null) {
            Iterator<?> i = policy.getAlternatives();
            while (i.hasNext() && algorithmSuite == null) {
                List<PolicyComponent> p = CastUtils.cast((List<?>)i.next());
                for (PolicyComponent p2 : p) {
                    if (p2 instanceof AbstractBinding) {
                        algorithmSuite = ((AbstractBinding)p2).getAlgorithmSuite();
                    }
                }
            }
        }
    }

    protected void setPolicyInternal(Element newPolicy) {
        this.setPolicyInternal(bus.getExtension(PolicyBuilder.class).getPolicy(newPolicy));
    }

    protected void setPolicyInternal(String policyReference) {
        PolicyBuilder builder = bus.getExtension(PolicyBuilder.class);
        ReferenceResolver resolver = new RemoteReferenceResolver(null, builder);
        PolicyRegistry registry = bus.getExtension(PolicyEngine.class).getRegistry();
        Policy resolved = registry.lookup(policyReference);
        if (null != resolved) {
            this.setPolicyInternal(resolved);
        } else {
            this.setPolicyInternal(resolver.resolveReference(policyReference));
        }
    }

    public Client getClient()  throws BusException, EndpointException {
        if (client == null) {
            createClient();
        }
        return client;
    }

    public void configureViaEPR(EndpointReferenceType ref, boolean useEPRWSAAddrAsMEXLocation) {
        if (client != null) {
            return;
        }
        location = EndpointReferenceUtils.getAddress(ref);
        if (location != null) {
            location = location.trim();
        }
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("EPR address: " + location);
        }

        final QName sName = EndpointReferenceUtils.getServiceName(ref, bus);
        if (sName != null) {
            serviceName = sName;
            final QName epName = EndpointReferenceUtils.getPortQName(ref, bus);
            if (epName != null) {
                endpointName = epName;
            }
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("EPR endpoint: " + serviceName + " " + endpointName);
            }
        }
        final String wsdlLoc = EndpointReferenceUtils.getWSDLLocation(ref);
        if (wsdlLoc != null) {
            wsdlLocation = wsdlLoc;
        }

        String mexLoc = findMEXLocation(ref, useEPRWSAAddrAsMEXLocation);
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("WS-MEX location: " + mexLoc);
        }
        if (mexLoc != null) {
            try {
                JaxWsProxyFactoryBean proxyFac = new JaxWsProxyFactoryBean();
                proxyFac.setBindingId(soapVersion);
                proxyFac.setAddress(mexLoc);
                MetadataExchange exc = proxyFac.create(MetadataExchange.class);
                Metadata metadata = exc.get2004();

                Definition definition = null;
                List<Schema> schemas = new ArrayList<>();
                // Parse the MetadataSections into WSDL definition + associated schemas
                for (MetadataSection s : metadata.getMetadataSection()) {
                    if ("http://schemas.xmlsoap.org/wsdl/".equals(s.getDialect())) {
                        definition =
                            bus.getExtension(WSDLManager.class).getDefinition((Element)s.getAny());
                    } else if ("http://www.w3.org/2001/XMLSchema".equals(s.getDialect())) {
                        Element schemaElement = getSchemaElement((Element)s.getAny(), s.getLocation());
                        if (schemaElement != null) {
                            QName schemaName =
                                new QName(schemaElement.getNamespaceURI(), schemaElement.getLocalName());
                            WSDLManager wsdlManager = bus.getExtension(WSDLManager.class);
                            ExtensibilityElement
                                exElement = wsdlManager.getExtensionRegistry().createExtension(Types.class, schemaName);
                            ((Schema)exElement).setElement(schemaElement);
                            schemas.add((Schema)exElement);
                        }
                    }
                }

                if (definition != null) {
                    // Add any extra schemas to the WSDL definition
                    for (Schema schema : schemas) {
                        definition.getTypes().addExtensibilityElement(schema);
                    }

                    WSDLServiceFactory factory = new WSDLServiceFactory(bus, definition);
                    SourceDataBinding dataBinding = new SourceDataBinding();
                    factory.setDataBinding(dataBinding);
                    Service service = factory.create();
                    service.setDataBinding(dataBinding);

                    // Get the endpoint + service names by matching the 'location' to the
                    // address in the WSDL. If the 'location' is 'anonymous' then just fall
                    // back to the first service + endpoint name in the WSDL, if the endpoint
                    // name is not defined in the Metadata
                    List<ServiceInfo> services = service.getServiceInfos();
                    String anonymousAddress = "http://www.w3.org/2005/08/addressing/anonymous";

                    if (!anonymousAddress.equals(location)) {
                        for (ServiceInfo serv : services) {
                            for (EndpointInfo ei : serv.getEndpoints()) {
                                if (ei.getAddress().equals(location)) {
                                    endpointName = ei.getName();
                                    serviceName = serv.getName();
                                    LOG.fine("Matched endpoint to location");
                                }
                            }
                        }
                    }

                    EndpointInfo ei = service.getEndpointInfo(endpointName);
                    if (ei == null && anonymousAddress.equals(location)
                        && !services.isEmpty() && !services.get(0).getEndpoints().isEmpty()) {
                        LOG.fine("Anonymous location so taking first endpoint");
                        serviceName = services.get(0).getName();
                        endpointName = services.get(0).getEndpoints().iterator().next().getName();
                        ei = service.getEndpointInfo(endpointName);
                    }

                    if (ei == null) {
                        throw new TrustException(LOG, "ADDRESS_NOT_MATCHED", location);
                    }

                    if (location != null && !anonymousAddress.equals(location)) {
                        ei.setAddress(location);
                    }
                    Endpoint endpoint = new EndpointImpl(bus, service, ei);
                    client = new ClientImpl(bus, endpoint);
                }
            } catch (Exception ex) {
                throw new TrustException("WS_MEX_ERROR", ex, LOG);
            }
        }
    }

    protected Element getSchemaElement(Element schemaElement, String schemaLocation) throws Exception {
        if (schemaElement == null) {
            if (!allowMexMetadataSchemaLocation) {
                LOG.info("Loading a schema from WS-MEX MetadataSection Location is disabled by "
                    + " default. Enable allowMexMetadataSchemaLocation to allow it.");
            } else {
                LOG.info("XSD schema location: " + schemaLocation);
                schemaElement = downloadSchema(schemaLocation);
            }
        }
        return schemaElement;
    }

    protected Element downloadSchema(String schemaLocation) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
        dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
        Document document = documentBuilder.parse(schemaLocation);
        return document.getDocumentElement();
    }

    protected String findMEXLocation(EndpointReferenceType ref, boolean useEPRWSAAddrAsMEXLocation) {
        if (ref.getMetadata() != null && ref.getMetadata().getAny() != null) {
            for (Object any : ref.getMetadata().getAny()) {
                if (any instanceof Element) {
                    String addr = findMEXLocation((Element)any);
                    if (addr != null) {
                        return addr;
                    }
                }
            }
        }
        return useEPRWSAAddrAsMEXLocation ? EndpointReferenceUtils.getAddress(ref) : null;
    }

    protected String findMEXLocation(Element ref) {
        Element el = DOMUtils.getFirstElement(ref);
        while (el != null) {
            if ("Address".equals(el.getLocalName())
                && VersionTransformer.isSupported(el.getNamespaceURI())
                && "MetadataReference".equals(ref.getLocalName())) {
                return DOMUtils.getContent(el);
            }
            String ad = findMEXLocation(el);
            if (ad != null) {
                return ad;
            }
            el = DOMUtils.getNextElement(el);
        }
        return null;
    }

    protected void createClient() throws BusException, EndpointException {
        if (client != null) {
            return;
        }

        if (wsdlLocation != null) {
            WSDLServiceFactory factory = new WSDLServiceFactory(bus, wsdlLocation, serviceName);
            SourceDataBinding dataBinding = new SourceDataBinding();
            factory.setDataBinding(dataBinding);
            Service service = factory.create();
            service.setDataBinding(dataBinding);
            EndpointInfo ei = service.getEndpointInfo(endpointName);
            Endpoint endpoint = new EndpointImpl(bus, service, ei);
            client = new ClientImpl(bus, endpoint);
        } else if (location != null) {
            Endpoint endpoint = STSUtils.createSTSEndpoint(bus, namespace, null, location, soapVersion,
                                                           policy, endpointName);

            client = new ClientImpl(bus, endpoint);
        } else {
            throw new TrustException(LOG, "NO_LOCATION");
        }

        client.getInFaultInterceptors().addAll(inFault);
        client.getInInterceptors().addAll(in);
        client.getOutInterceptors().addAll(out);
        client.getOutFaultInterceptors().addAll(outFault);

        if (tlsClientParameters != null) {
            HTTPConduit http = (HTTPConduit) client.getConduit();
            http.setTlsClientParameters(tlsClientParameters);
        }

        in = null;
        out = null;
        inFault = null;
        outFault = null;
        if (features != null) {
            for (Feature f : features) {
                f.initialize(client, bus);
            }
        }
    }

    protected BindingOperationInfo findOperation(String suffix) {
        BindingInfo bi = client.getEndpoint().getBinding().getBindingInfo();
        for (BindingOperationInfo boi : bi.getOperations()) {
            SoapOperationInfo soi = boi.getExtensor(SoapOperationInfo.class);
            String soapAction = soi != null ? soi.getAction() : null;
            Object o = boi.getOperationInfo().getInput()
                    .getExtensionAttribute(new QName("http://www.w3.org/2007/05/addressing/metadata",
                                                     "Action"));
            if (o instanceof QName) {
                o = ((QName)o).getLocalPart();
            }
            String wsamAction = o == null ? null : o.toString();

            if ((soapAction != null && soapAction.endsWith(suffix))
                || (wsamAction != null && wsamAction.endsWith(suffix))) {
                PolicyEngine pe = bus.getExtension(PolicyEngine.class);
                Conduit conduit = client.getConduit();
                EffectivePolicy effectivePolicy
                    = pe.getEffectiveClientRequestPolicy(client.getEndpoint().getEndpointInfo(),
                                                         boi, conduit,
                                                         PhaseInterceptorChain.getCurrentMessage());
                setPolicyInternal(effectivePolicy.getPolicy());
                return boi;
            }
        }
        //operation is not correct as the Action is not set correctly.   Let's see if
        //we can at least find it by name and then set the action and such manually later.
        for (BindingOperationInfo boi : bi.getOperations()) {
            if (suffix.endsWith(boi.getName().getLocalPart())) {
                return boi;
            }
        }
        //Still didn't find anything useful
        for (BindingOperationInfo boi : bi.getOperations()) {
            if (boi.getInput().getMessageInfo().getMessagePartsNumber() > 0) {
                MessagePartInfo mpi = boi.getInput().getMessageInfo().getFirstMessagePart();
                if ("RequestSecurityToken".equals(mpi.getConcreteName().getLocalPart())) {
                    return boi;
                }
            }
        }
        return null;
    }

    /**
     * Make an "Issue" invocation and return the response as a STSResponse Object
     */
    protected STSResponse issue(
        String appliesTo, String action, String requestType, String binaryExchange
    ) throws Exception {
        createClient();
        BindingOperationInfo boi = findOperation("/RST/Issue");

        client.getRequestContext().putAll(ctx);
        if (action != null) {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION, action);
        } else if (isSecureConv) {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION,
                                           namespace + "/RST/SCT");
        } else {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION,
                                           namespace + "/RST/Issue");
        }

        W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
        writer.writeStartElement("wst", "RequestSecurityToken", namespace);
        writer.writeNamespace("wst", namespace);
        if (context != null) {
            writer.writeAttribute(null, "Context", context);
        }

        boolean wroteKeySize = false;
        String keyTypeTemplate = null;
        String sptt = null;

        if (template != null && DOMUtils.getFirstElement(template) != null) {
            if (this.useSecondaryParameters()) {
                writer.writeStartElement("wst", "SecondaryParameters", namespace);
            }

            Element tl = DOMUtils.getFirstElement(template);
            while (tl != null) {
                StaxUtils.copy(tl, writer);
                if ("KeyType".equals(tl.getLocalName())) {
                    keyTypeTemplate = DOMUtils.getContent(tl);
                } else if ("KeySize".equals(tl.getLocalName())) {
                    wroteKeySize = true;
                    keySize = Integer.parseInt(DOMUtils.getContent(tl));
                } else if ("TokenType".equals(tl.getLocalName())) {
                    sptt = DOMUtils.getContent(tl);
                }
                tl = DOMUtils.getNextElement(tl);
            }

            if (this.useSecondaryParameters()) {
                writer.writeEndElement();
            }
        }

        if (isSpnego) {
            tokenType = STSUtils.getTokenTypeSCT(namespace);
            sendKeyType = false;
        }

        if (sptt == null) {
            addTokenType(writer);
        }

        addRequestType(requestType, writer);
        if (enableAppliesTo) {
            addAppliesTo(writer, appliesTo);
        }

        addClaims(writer);

        if (isSecureConv || enableLifetime) {
            addLifetime(writer);
        }

        // Write out renewal semantics
        writeRenewalSemantics(writer);

        Element onBehalfOfToken = getOnBehalfOfToken();
        if (onBehalfOfToken != null) {
            writer.writeStartElement("wst", "OnBehalfOf", namespace);
            StaxUtils.copy(onBehalfOfToken, writer);
            writer.writeEndElement();
        }

        if (keyTypeTemplate == null) {
            keyTypeTemplate = writeKeyType(writer, keyType);
        }

        byte[] requestorEntropy = null;
        X509Certificate cert = null;
        Crypto crypto = null;

        if (keySize <= 0) {
            keySize = 256;
        }
        if (keyTypeTemplate != null && keyTypeTemplate.endsWith("SymmetricKey")) {
            requestorEntropy = writeElementsForRSTSymmetricKey(writer, wroteKeySize);
        } else if (keyTypeTemplate != null && keyTypeTemplate.endsWith("PublicKey")) {
            // Use the given cert, or else get it from a Crypto instance
            if (useKeyCertificate != null) {
                cert = useKeyCertificate;
            } else {
                crypto = createCrypto(false);
                cert = getCert(crypto);
            }
            writeElementsForRSTPublicKey(writer, cert);
        } else if (isSpnego || isSecureConv) {
            addKeySize(keySize, writer);
        }

        if (binaryExchange != null) {
            addBinaryExchange(binaryExchange, writer);
        }

        Element actAsSecurityToken = getActAsToken();
        if (actAsSecurityToken != null) {
            writer.writeStartElement(STSUtils.WST_NS_08_02, "ActAs");
            StaxUtils.copy(actAsSecurityToken, writer);
            writer.writeEndElement();
        }

        Element customElement = getCustomContent();
        if (customElement != null) {
            StaxUtils.copy(customElement, writer);
        }

        writer.writeEndElement();

        Object[] obj = client.invoke(boi, new DOMSource(writer.getDocument().getDocumentElement()));

        @SuppressWarnings("unchecked")
        Collection<Attachment> attachments =
        (Collection<Attachment>)client.getResponseContext().get(Message.ATTACHMENTS);
        return new STSResponse((DOMSource)obj[0], requestorEntropy, cert, crypto, attachments);
    }

    /**
     * Get the "OnBehalfOf" element to be sent to the STS.
     */
    public Element getOnBehalfOfToken() throws Exception {
        return getDelegationSecurityToken(this.onBehalfOf);
    }

    /**
     * Get the "ActAs" element to be sent to the STS.
     */
    public Element getActAsToken() throws Exception {
        return getDelegationSecurityToken(this.actAs);
    }

    /**
     * Get some custom Element to be inserted into the RequestSecurityToken
     */
    public Element getCustomContent() throws Exception {
        if (customContent != null) {
            // We can also support a CallbackHandler here as per getDelegationSecurityToken if required
            final boolean isString = customContent instanceof String;
            final boolean isElement = customContent instanceof Element;
            if (isString) {
                final Document doc =
                    StaxUtils.read(new StringReader((String) customContent));
                return doc.getDocumentElement();
            } else if (isElement) {
                return (Element) customContent;
            }
        }
        return null;
    }

    protected Element getDelegationSecurityToken(Object delegationObject) throws Exception {
        if (delegationObject != null) {
            final boolean isString = delegationObject instanceof String;
            final boolean isElement = delegationObject instanceof Element;
            final boolean isCallbackHandler = delegationObject instanceof CallbackHandler;
            if (isString) {
                final Document doc =
                    StaxUtils.read(new StringReader((String) delegationObject));
                return doc.getDocumentElement();
            } else if (isElement) {
                return (Element) delegationObject;
            } else if (isCallbackHandler) {
                DelegationCallback callback = new DelegationCallback(message);
                ((CallbackHandler)delegationObject).handle(new Callback[]{callback});
                return callback.getToken();
            }
        }
        return null;
    }

    protected byte[] writeElementsForRSTSymmetricKey(W3CDOMStreamWriter writer,
            boolean wroteKeySize) throws Exception {
        byte[] requestorEntropy = null;

        if (!wroteKeySize) {
            addKeySize(keySize, writer);
        }

        if (requiresEntropy) {
            writer.writeStartElement("wst", "Entropy", namespace);
            writer.writeStartElement("wst", "BinarySecret", namespace);
            writer.writeAttribute("Type", namespace + "/Nonce");
            if (algorithmSuite == null) {
                requestorEntropy = WSSecurityUtil.generateNonce(keySize / 8);
            } else {
                AlgorithmSuiteType algType = algorithmSuite.getAlgorithmSuiteType();
                requestorEntropy = WSSecurityUtil
                    .generateNonce(algType.getMaximumSymmetricKeyLength() / 8);
            }
            writer.writeCharacters(org.apache.xml.security.utils.XMLUtils.encodeToString(requestorEntropy));

            writer.writeEndElement();
            writer.writeEndElement();
            writer.writeStartElement("wst", "ComputedKeyAlgorithm", namespace);
            writer.writeCharacters(namespace + "/CK/PSHA1");
            writer.writeEndElement();
        }
        return requestorEntropy;
    }


    protected void writeElementsForRSTPublicKey(W3CDOMStreamWriter writer,
            X509Certificate cert) throws Exception {
        writer.writeStartElement("wst", "UseKey", namespace);
        writer.writeStartElement("ds", "KeyInfo", "http://www.w3.org/2000/09/xmldsig#");
        writer.writeNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");

        boolean useCert = useCertificateForConfirmationKeyInfo;
        String useCertStr = (String)getProperty(SecurityConstants.STS_TOKEN_USE_CERT_FOR_KEYINFO);
        if (useCertStr != null) {
            useCert = Boolean.parseBoolean(useCertStr);
        }
        if (useCert) {
            X509Data certElem = new X509Data(writer.getDocument());
            certElem.addCertificate(cert);
            writer.getCurrentNode().appendChild(certElem.getElement());
        } else {
            writer.writeStartElement("ds", "KeyValue", "http://www.w3.org/2000/09/xmldsig#");
            PublicKey key = cert.getPublicKey();
            String pubKeyAlgo = key.getAlgorithm();
            if ("DSA".equalsIgnoreCase(pubKeyAlgo)) {
                DSAKeyValue dsaKeyValue = new DSAKeyValue(writer.getDocument(), key);
                writer.getCurrentNode().appendChild(dsaKeyValue.getElement());
            } else if ("RSA".equalsIgnoreCase(pubKeyAlgo)) {
                RSAKeyValue rsaKeyValue = new RSAKeyValue(writer.getDocument(), key);
                writer.getCurrentNode().appendChild(rsaKeyValue.getElement());
            }
            writer.writeEndElement();
        }

        writer.writeEndElement();
        writer.writeEndElement();
    }

    protected void addBinaryExchange(
        String binaryExchange,
        W3CDOMStreamWriter writer
    ) throws XMLStreamException {
        writer.writeStartElement("wst", "BinaryExchange", namespace);
        writer.writeAttribute("EncodingType", WSS4JConstants.BASE64_ENCODING);
        writer.writeAttribute("ValueType", namespace + "/spnego");
        writer.writeCharacters(binaryExchange);
        writer.writeEndElement();
    }

    protected void addKeySize(int keysize, W3CDOMStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement("wst", "KeySize", namespace);
        writer.writeCharacters(Integer.toString(keysize));
        writer.writeEndElement();
    }

    protected void addRequestType(String requestType, W3CDOMStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement("wst", "RequestType", namespace);
        writer.writeCharacters(namespace + requestType);
        writer.writeEndElement();
    }

    protected Element getDocumentElement(DOMSource ds) {
        Node nd = ds.getNode();
        if (nd instanceof Document) {
            nd = ((Document)nd).getDocumentElement();
        }
        return (Element)nd;
    }

    /**
     * Make an "Renew" invocation and return the response as a STSResponse Object
     */
    public STSResponse renew(SecurityToken tok) throws Exception {
        createClient();
        BindingOperationInfo boi = findOperation("/RST/Renew");

        client.getRequestContext().putAll(ctx);
        client.getRequestContext().remove(SecurityConstants.TOKEN_ID);
        if (isSecureConv) {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION, namespace + "/RST/SCT/Renew");
        } else {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION, namespace + "/RST/Renew");
        }

        W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
        writer.writeStartElement("wst", "RequestSecurityToken", namespace);
        writer.writeNamespace("wst", namespace);
        if (context != null) {
            writer.writeAttribute(null, "Context", context);
        }

        String sptt = null;
        if (template != null && DOMUtils.getFirstElement(template) != null) {
            if (this.useSecondaryParameters()) {
                writer.writeStartElement("wst", "SecondaryParameters", namespace);
            }

            Element tl = DOMUtils.getFirstElement(template);
            while (tl != null) {
                StaxUtils.copy(tl, writer);
                if ("TokenType".equals(tl.getLocalName())) {
                    sptt = DOMUtils.getContent(tl);
                }
                tl = DOMUtils.getNextElement(tl);
            }

            if (this.useSecondaryParameters()) {
                writer.writeEndElement();
            }
        }

        if (isSpnego) {
            tokenType = STSUtils.getTokenTypeSCT(namespace);
        }

        if (sptt == null) {
            addTokenType(writer);
        }

        addRequestType("/Renew", writer);
        if (enableAppliesTo) {
            addAppliesTo(writer, tok.getIssuerAddress());
        }

        if (isSecureConv || enableLifetime) {
            addLifetime(writer);
        }

        writer.writeStartElement("wst", "RenewTarget", namespace);
        StaxUtils.copy(tok.getToken(), writer);
        writer.writeEndElement();

        // Write out renewal semantics
        writeRenewalSemantics(writer);

        writer.writeEndElement();

        Object[] obj = client.invoke(boi, new DOMSource(writer.getDocument().getDocumentElement()));

        @SuppressWarnings("unchecked")
        Collection<Attachment> attachments =
            (Collection<Attachment>)client.getResponseContext().get(Message.ATTACHMENTS);
        return new STSResponse((DOMSource)obj[0], null, null, null, attachments);
    }

    protected PrimitiveAssertion getAddressingAssertion() {
        String ns = "http://schemas.xmlsoap.org/ws/2004/08/addressing/policy";
        String local = "UsingAddressing";
        if ("http://www.w3.org/2005/08/addressing".equals(addressingNamespace)) {
            ns = "http://www.w3.org/2007/02/addressing/metadata";
            local = "Addressing";
        }
        return new PrimitiveAssertion(new QName(ns, local), true);
    }

    /**
     * Make an "Validate" invocation and return the response as a STSResponse Object
     */
    protected STSResponse validate(SecurityToken tok, String tokentype)
        throws Exception {
        createClient();

        if (tokentype == null) {
            tokentype = tokenType;
        }
        if (tokentype == null) {
            tokentype = namespace + "/RSTR/Status";
        }

        Policy validatePolicy = new Policy();
        ExactlyOne one = new ExactlyOne();
        validatePolicy.addPolicyComponent(one);
        All all = new All();
        one.addPolicyComponent(all);
        all.addAssertion(getAddressingAssertion());

        client.getRequestContext().clear();
        client.getRequestContext().putAll(ctx);
        client.getRequestContext().put(SecurityConstants.TOKEN, tok);
        BindingOperationInfo boi = findOperation("/RST/Validate");
        if (boi == null) {
            boi = findOperation("/RST/Issue");
            client.getRequestContext().put(PolicyConstants.POLICY_OVERRIDE, validatePolicy);
        }

        client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION,
                                       namespace + "/RST/Validate");


        W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
        writer.writeStartElement("wst", "RequestSecurityToken", namespace);
        writer.writeNamespace("wst", namespace);

        writer.writeStartElement("wst", "TokenType", namespace);
        writer.writeCharacters(tokentype);
        writer.writeEndElement();

        writer.writeStartElement("wst", "RequestType", namespace);
        writer.writeCharacters(namespace + "/Validate");
        writer.writeEndElement();

        if (tokentype.endsWith("/RSTR/Status")) {
            addClaims(writer);

            writer.writeStartElement("wst", "ValidateTarget", namespace);

            Element el = tok.getToken();
            if (el != null) {
                StaxUtils.copy(el, writer);
            }

            writer.writeEndElement();
            writer.writeEndElement();

            Object[] o = client.invoke(boi, new DOMSource(writer.getDocument().getDocumentElement()));

            @SuppressWarnings("unchecked")
            Collection<Attachment> attachments =
                (Collection<Attachment>)client.getResponseContext().get(Message.ATTACHMENTS);
            return new STSResponse((DOMSource)o[0], null, null, null, attachments);
        }
        if (enableLifetime) {
            addLifetime(writer);
        }

        // Default to Bearer KeyType
        String keyTypeTemplate = keyType;
        if (keyTypeTemplate == null) {
            keyTypeTemplate = namespace + "/Bearer";
        }
        keyTypeTemplate = writeKeyType(writer, keyTypeTemplate);

        byte[] requestorEntropy = null;
        X509Certificate cert = null;
        Crypto crypto = null;

        if (keySize <= 0) {
            keySize = 256;
        }
        if (keyTypeTemplate != null && keyTypeTemplate.endsWith("SymmetricKey")) {
            requestorEntropy = writeElementsForRSTSymmetricKey(writer, false);
        } else if (keyTypeTemplate != null && keyTypeTemplate.endsWith("PublicKey")) {
            // Use the given cert, or else get it from a Crypto instance
            if (useKeyCertificate != null) {
                cert = useKeyCertificate;
            } else {
                crypto = createCrypto(false);
                cert = getCert(crypto);
            }
            writeElementsForRSTPublicKey(writer, cert);
        }

        writeRenewalSemantics(writer);

        addClaims(writer);

        writer.writeStartElement("wst", "ValidateTarget", namespace);

        Element el = tok.getToken();
        StaxUtils.copy(el, writer);

        writer.writeEndElement();
        writer.writeEndElement();

        Object[] o = client.invoke(boi, new DOMSource(writer.getDocument().getDocumentElement()));

        @SuppressWarnings("unchecked")
        Collection<Attachment> attachments =
            (Collection<Attachment>)client.getResponseContext().get(Message.ATTACHMENTS);
        return new STSResponse((DOMSource)o[0], requestorEntropy, cert, crypto, attachments);
    }

    private void writeRenewalSemantics(XMLStreamWriter writer) throws XMLStreamException {
        // Write out renewal semantics
        if (sendRenewing) {
            writer.writeStartElement("wst", "Renewing", namespace);
            if (!allowRenewing) {
                writer.writeAttribute(null, "Allow", "false");
            }
            if (allowRenewing && allowRenewingAfterExpiry) {
                writer.writeAttribute(null, "OK", "true");
            }
            writer.writeEndElement();
        }
    }

    /**
     * Make an "Cancel" invocation and return the response as a STSResponse Object
     */
    protected STSResponse cancel(SecurityToken token) throws Exception {
        createClient();

        client.getRequestContext().clear();
        client.getRequestContext().putAll(ctx);
        client.getRequestContext().put(SecurityConstants.TOKEN, token);

        BindingOperationInfo boi = findOperation("/RST/Cancel");
        boolean attachTokenDirectly = true;
        if (boi == null) {
            attachTokenDirectly = false;
            boi = findOperation("/RST/Issue");

            Policy cancelPolicy = new Policy();
            ExactlyOne one = new ExactlyOne();
            cancelPolicy.addPolicyComponent(one);
            All all = new All();
            one.addPolicyComponent(all);
            all.addAssertion(getAddressingAssertion());

            final SecureConversationToken secureConversationToken =
                new SecureConversationToken(
                    SPConstants.SPVersion.SP12,
                    SPConstants.IncludeTokenType.INCLUDE_TOKEN_ALWAYS_TO_RECIPIENT,
                    null,
                    null,
                    null,
                    null
                );
            secureConversationToken.setOptional(true);

            class InternalProtectionToken extends ProtectionToken {
                InternalProtectionToken(SPVersion version, Policy nestedPolicy) {
                    super(version, nestedPolicy);
                    super.setToken(secureConversationToken);
                }
            }

            DefaultSymmetricBinding binding =
                new DefaultSymmetricBinding(SPConstants.SPVersion.SP12, new Policy());
            all.addAssertion(binding);
            all.addAssertion(getAddressingAssertion());
            binding.setProtectionToken(
                new InternalProtectionToken(SPConstants.SPVersion.SP12, new Policy())
            );
            binding.setIncludeTimestamp(true);
            binding.setOnlySignEntireHeadersAndBody(true);
            binding.setProtectTokens(false);

            String addrNamespace = addressingNamespace;
            if (addrNamespace == null) {
                addrNamespace = "http://www.w3.org/2005/08/addressing";
            }

            List<Header> headers = new ArrayList<>();
            headers.add(new Header("To", addrNamespace));
            headers.add(new Header("From", addrNamespace));
            headers.add(new Header("FaultTo", addrNamespace));
            headers.add(new Header("ReplyTo", addrNamespace));
            headers.add(new Header("Action", addrNamespace));
            headers.add(new Header("MessageID", addrNamespace));
            headers.add(new Header("RelatesTo", addrNamespace));

            SignedParts parts = new SignedParts(SPConstants.SPVersion.SP12, true, null, headers, false);
            parts.setOptional(true);
            all.addPolicyComponent(parts);

            client.getRequestContext().put(PolicyConstants.POLICY_OVERRIDE, cancelPolicy);
        }

        if (isSecureConv) {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION,
                                           namespace + "/RST/SCT/Cancel");
        } else {
            client.getRequestContext().put(SoapBindingConstants.SOAP_ACTION,
                                           namespace + "/RST/Cancel");
        }

        W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
        writer.writeStartElement("wst", "RequestSecurityToken", namespace);
        writer.writeNamespace("wst", namespace);
        writer.writeStartElement("wst", "RequestType", namespace);
        writer.writeCharacters(namespace + "/Cancel");
        writer.writeEndElement();

        writer.writeStartElement("wst", "CancelTarget", namespace);
        Element el;
        if (attachTokenDirectly) {
            el = token.getToken();
        } else {
            el = token.getUnattachedReference();
            if (el == null) {
                el = token.getAttachedReference();
            }
        }
        StaxUtils.copy(el, writer);

        writer.writeEndElement();
        writer.writeEndElement();

        Object[] obj = client.invoke(boi, new DOMSource(writer.getDocument().getDocumentElement()));
        return new STSResponse((DOMSource)obj[0], null);
    }

    protected boolean useSecondaryParameters() {
        return !STSUtils.WST_NS_05_02.equals(namespace);
    }

    protected String writeKeyType(W3CDOMStreamWriter writer, String keyTypeToWrite)
        throws XMLStreamException {
        if (isSecureConv) {
            if (keyTypeToWrite == null) {
                writer.writeStartElement("wst", "TokenType", namespace);
                writer.writeCharacters(STSUtils.getTokenTypeSCT(namespace));
                writer.writeEndElement();
                keyTypeToWrite = namespace + "/SymmetricKey";
            }
        } else if (keyTypeToWrite == null && sendKeyType) {
            writer.writeStartElement("wst", "KeyType", namespace);
            writer.writeCharacters(namespace + "/SymmetricKey");
            writer.writeEndElement();
            keyTypeToWrite = namespace + "/SymmetricKey";
        } else if (keyTypeToWrite != null) {
            writer.writeStartElement("wst", "KeyType", namespace);
            writer.writeCharacters(keyTypeToWrite);
            writer.writeEndElement();
        }
        return keyTypeToWrite;
    }

    protected X509Certificate getCert(Crypto crypto) throws Exception {
        if (crypto == null) {
            throw new Fault("No Crypto token properties are available to retrieve a certificate",
                            LOG);
        }

        String alias = (String)getProperty(SecurityConstants.STS_TOKEN_USERNAME);
        if (alias == null) {
            alias = crypto.getDefaultX509Identifier();
        }
        if (alias == null) {
            throw new Fault("No alias specified for retrieving PublicKey", LOG);
        }
        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
        cryptoType.setAlias(alias);

        X509Certificate[] certs = crypto.getX509Certificates(cryptoType);
        if (certs == null || certs.length == 0) {
            throw new Fault("Could not get X509Certificate for alias " + alias, LOG);
        }
        return certs[0];
    }

    protected void addLifetime(XMLStreamWriter writer) throws XMLStreamException {
        Instant created = Instant.now();
        Instant expires = created.plusSeconds(ttl);

        writer.writeStartElement("wst", "Lifetime", namespace);
        writer.writeNamespace("wsu", WSS4JConstants.WSU_NS);
        writer.writeStartElement("wsu", "Created", WSS4JConstants.WSU_NS);
        writer.writeCharacters(created.atZone(ZoneOffset.UTC).format(DateUtil.getDateTimeFormatter(true)));
        writer.writeEndElement();

        writer.writeStartElement("wsu", "Expires", WSS4JConstants.WSU_NS);
        writer.writeCharacters(expires.atZone(ZoneOffset.UTC).format(DateUtil.getDateTimeFormatter(true)));
        writer.writeEndElement();
        writer.writeEndElement();
    }

    protected void addAppliesTo(XMLStreamWriter writer, String appliesTo) throws XMLStreamException {
        if (appliesTo != null && addressingNamespace != null) {
            String policyNS = wspNamespace;
            if (policyNS == null) {
                policyNS = "http://schemas.xmlsoap.org/ws/2004/09/policy";
            }
            writer.writeStartElement("wsp", "AppliesTo", policyNS);
            writer.writeNamespace("wsp", policyNS);
            writer.writeStartElement("wsa", "EndpointReference", addressingNamespace);
            writer.writeNamespace("wsa", addressingNamespace);
            writer.writeStartElement("wsa", "Address", addressingNamespace);
            writer.writeCharacters(appliesTo);
            writer.writeEndElement();
            writer.writeEndElement();
            writer.writeEndElement();
        }
    }

    protected void addTokenType(XMLStreamWriter writer) throws XMLStreamException {
        if (tokenType != null) {
            writer.writeStartElement("wst", "TokenType", namespace);
            writer.writeCharacters(tokenType);
            writer.writeEndElement();
        }
    }

    protected void addClaims(XMLStreamWriter writer) throws Exception {
        Object claimsToSerialize = claims;
        if (claimsToSerialize == null && claimsCallbackHandler != null) {
            ClaimsCallback callback = new ClaimsCallback(message);
            claimsCallbackHandler.handle(new Callback[]{callback});
            claimsToSerialize = callback.getClaims();
        }

        if (claimsToSerialize instanceof Element) {
            StaxUtils.copy((Element)claimsToSerialize, writer);
        } else if (claimsToSerialize instanceof ClaimCollection) {
            ClaimCollection claimCollection = (ClaimCollection)claimsToSerialize;
            claimCollection.serialize(writer, "wst", namespace);
        }
    }

    protected SecurityToken createSecurityToken(Element el, byte[] requestorEntropy)
        throws WSSecurityException, Base64DecodingException {

        if ("RequestSecurityTokenResponseCollection".equals(el.getLocalName())) {
            el = DOMUtils.getFirstElement(el);
        }
        if (!"RequestSecurityTokenResponse".equals(el.getLocalName())) {
            throw new Fault("Unexpected element " + el.getLocalName(), LOG);
        }
        el = DOMUtils.getFirstElement(el);
        Element rst = null;
        Element rar = null;
        Element rur = null;
        Element rpt = null;
        Element lte = null;
        Element entropy = null;
        String tt = null;
        String retKeySize = null;

        while (el != null) {
            String ln = el.getLocalName();
            if (namespace.equals(el.getNamespaceURI())) {
                if ("Lifetime".equals(ln)) {
                    lte = el;
                } else if ("RequestedSecurityToken".equals(ln)) {
                    rst = DOMUtils.getFirstElement(el);
                } else if ("RequestedAttachedReference".equals(ln)) {
                    rar = DOMUtils.getFirstElement(el);
                } else if ("RequestedUnattachedReference".equals(ln)) {
                    rur = DOMUtils.getFirstElement(el);
                } else if ("RequestedProofToken".equals(ln)) {
                    rpt = el;
                } else if ("Entropy".equals(ln)) {
                    entropy = el;
                } else if ("TokenType".equals(ln)) {
                    tt = DOMUtils.getContent(el);
                } else if ("KeySize".equals(ln)) {
                    retKeySize = DOMUtils.getContent(el);
                }
            }
            el = DOMUtils.getNextElement(el);
        }
        Element rstDec = rst;
        String id = findID(rar, rur, rstDec);
        if (StringUtils.isEmpty(id)) {
            LOG.fine("No ID extracted from token, so just making one up");
            id = WSSConfig.getNewInstance().getIdAllocator().createSecureId("_", null);
        }
        SecurityToken token = new SecurityToken(id, rstDec, lte);
        token.setAttachedReference(rar);
        token.setUnattachedReference(rur);
        token.setIssuerAddress(location);
        token.setTokenType(tt);

        byte[] secret = null;

        if (rpt != null) {
            Element child = DOMUtils.getFirstElement(rpt);
            QName childQname = DOMUtils.getElementQName(child);
            if (childQname.equals(new QName(namespace, "BinarySecret"))) {
                // First check for the binary secret
                String b64Secret = DOMUtils.getContent(child);
                secret = org.apache.xml.security.utils.XMLUtils.decode(b64Secret);
            } else if (childQname.equals(new QName(WSS4JConstants.ENC_NS, WSS4JConstants.ENC_KEY_LN))) {
                secret = decryptKey(child);
            } else if (childQname.equals(new QName(namespace, "ComputedKey"))) {
                // Handle the computed key
                Element computedKeyChild = entropy == null ? null : DOMUtils.getFirstElement(entropy);
                byte[] serviceEntr = null;

                if (computedKeyChild != null) {
                    QName computedKeyChildQName = DOMUtils.getElementQName(computedKeyChild);
                    if (computedKeyChildQName.equals(new QName(WSS4JConstants.ENC_NS, WSS4JConstants.ENC_KEY_LN))) {
                        serviceEntr = decryptKey(computedKeyChild);
                    } else if (computedKeyChildQName.equals(new QName(namespace, "BinarySecret"))) {
                        String content = DOMUtils.getContent(computedKeyChild);
                        serviceEntr = org.apache.xml.security.utils.XMLUtils.decode(content);
                    }
                }

                if (serviceEntr != null) {
                    // Right now we only use PSHA1 as the computed key algo
                    P_SHA1 psha1 = new P_SHA1();

                    int length = 0;
                    if (retKeySize != null) {
                        try {
                            length = Integer.parseInt(retKeySize);
                        } catch (NumberFormatException ex) {
                            // do nothing
                        }
                    } else {
                        length = keySize;
                    }
                    if (length <= 0) {
                        length = 256;
                    }
                    try {
                        secret = psha1.createKey(requestorEntropy, serviceEntr, 0, length / 8);
                    } catch (WSSecurityException e) {
                        throw new TrustException("DERIVED_KEY_ERROR", e, LOG);
                    }
                } else {
                    // Service entropy missing
                    throw new TrustException("NO_ENTROPY", LOG);
                }
            }
        } else if (requestorEntropy != null) {
            // Use requester entropy as the key
            secret = requestorEntropy;
        }
        token.setSecret(secret);

        return token;
    }

    protected byte[] decryptKey(Element child) throws TrustException, WSSecurityException, Base64DecodingException {
        String encryptionAlgorithm = X509Util.getEncAlgo(child);
        // For the SPNEGO case just return the decoded cipher value and decrypt it later
        if (encryptionAlgorithm != null && encryptionAlgorithm.endsWith("spnego#GSS_Wrap")) {
            // Get the CipherValue
            Element tmpE =
                XMLUtils.getDirectChildElement(child, "CipherData", WSS4JConstants.ENC_NS);
            byte[] cipherValue = null;
            if (tmpE != null) {
                tmpE =
                    XMLUtils.getDirectChildElement(tmpE, "CipherValue", WSS4JConstants.ENC_NS);
                if (tmpE != null) {
                    String content = DOMUtils.getContent(tmpE);
                    cipherValue = org.apache.xml.security.utils.XMLUtils.decode(content);
                }
            }
            if (cipherValue == null) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY, "noCipher");
            }
            return cipherValue;
        }
        try {
            EncryptedKeyProcessor proc = new EncryptedKeyProcessor();
            WSDocInfo docInfo = new WSDocInfo(child.getOwnerDocument());
            RequestData data = new RequestData();
            data.setWssConfig(WSSConfig.getNewInstance());
            data.setDecCrypto(createCrypto(true));
            data.setCallbackHandler(createHandler());
            data.setWsDocInfo(docInfo);
            List<WSSecurityEngineResult> result = proc.handleToken(child, data);
            return
                (byte[])result.get(0).get(
                    WSSecurityEngineResult.TAG_SECRET
                );
        } catch (IOException e) {
            throw new TrustException("ENCRYPTED_KEY_ERROR", e, LOG);
        }
    }

    protected CallbackHandler createHandler() {
        Object o = getProperty(SecurityConstants.CALLBACK_HANDLER);
        try {
            return SecurityUtils.getCallbackHandler(o);
        } catch (Exception e) {
            throw new Fault(e);
        }
    }

    protected Object getProperty(String s) {
        String key = s;

        Object o = ctx.get(key);
        if (o == null) {
            o = client.getEndpoint().getEndpointInfo().getProperty(key);
        }
        if (o == null) {
            o = client.getEndpoint().getEndpointInfo().getBinding().getProperty(key);
        }
        if (o == null) {
            o = client.getEndpoint().getService().get(key);
        }

        key = "ws-" + s;
        if (o == null) {
            o = ctx.get(key);
        }
        if (o == null) {
            o = client.getEndpoint().getEndpointInfo().getProperty(key);
        }
        if (o == null) {
            o = client.getEndpoint().getEndpointInfo().getBinding().getProperty(key);
        }
        if (o == null) {
            o = client.getEndpoint().getService().get(key);
        }

        return o;
    }

    protected Crypto createCrypto(boolean decrypt) throws IOException, WSSecurityException {
        Crypto crypto = (Crypto)getProperty(SecurityConstants.STS_TOKEN_CRYPTO + (decrypt ? ".decrypt" : ""));
        if (crypto != null) {
            return crypto;
        }

        Object o = getProperty(SecurityConstants.STS_TOKEN_PROPERTIES + (decrypt ? ".decrypt" : ""));

        URL propsURL = SecurityUtils.loadResource(message, o);
        Properties properties = WSS4JUtils.getProps(o, propsURL);

        if (properties != null) {
            PasswordEncryptor passwordEncryptor = WSS4JUtils.getPasswordEncryptor(message);
            return CryptoFactory.getInstance(properties, this.getClass().getClassLoader(), passwordEncryptor);
        }
        if (decrypt) {
            return createCrypto(false);
        }
        return null;
    }

    protected String findID(Element rar, Element rur, Element rst) {
        String id = null;
        if (rst != null) {
            QName elName = DOMUtils.getElementQName(rst);
            if (elName.equals(new QName(WSS4JConstants.SAML_NS, "Assertion"))
                && rst.hasAttributeNS(null, "AssertionID")) {
                id = rst.getAttributeNS(null, "AssertionID");
            } else if (elName.equals(new QName(WSS4JConstants.SAML2_NS, "Assertion"))
                && rst.hasAttributeNS(null, "ID")) {
                id = rst.getAttributeNS(null, "ID");
            }
            if (id == null || "".equals(id)) {
                id = this.getIDFromSTR(rst);
            }
        }
        if ((id == null || "".equals(id)) && rar != null) {
            id = this.getIDFromSTR(rar);
        }
        if ((id == null || "".equals(id)) && rur != null) {
            id = this.getIDFromSTR(rur);
        }
        if ((id == null || "".equals(id)) && rst != null) {
            id = rst.getAttributeNS(WSS4JConstants.WSU_NS, "Id");
            if (id == null || "".equals(id)) {
                QName elName = DOMUtils.getElementQName(rst);
                if (elName.equals(new QName(WSS4JConstants.SAML2_NS, "EncryptedAssertion"))) {
                    Element child = DOMUtils.getFirstElement(rst);
                    if (child != null) {
                        id = child.getAttributeNS(WSS4JConstants.WSU_NS, "Id");
                    }
                }
            }
        }
        return id;
    }

    protected String getIDFromSTR(Element el) {
        Element child = DOMUtils.getFirstElement(el);
        if (child == null) {
            return null;
        }
        QName elName = DOMUtils.getElementQName(child);
        if (elName.equals(new QName(WSS4JConstants.SIG_NS, "KeyInfo"))
            || elName.equals(new QName(WSS4JConstants.WSSE_NS, "KeyIdentifier"))) {
            return DOMUtils.getContent(child);
        } else if (elName.equals(Reference.TOKEN)) {
            return child.getAttributeNS(null, "URI");
        } else if (elName.equals(new QName(STSUtils.SCT_NS_05_02, "Identifier"))
                   || elName.equals(new QName(STSUtils.SCT_NS_05_12, "Identifier"))) {
            return DOMUtils.getContent(child);
        }
        return null;
    }

    public void setTemplate(Element rstTemplate) {
        template = rstTemplate;
    }

    /**
     * Set a Claims Object to be included in the request. This Object can be either a DOM Element,
     * which will be copied "as is" into the request, or else a
     * org.apache.cxf.rt.security.claims.ClaimCollection Object.
     */
    public void setClaims(Object rstClaims) {
        claims = rstClaims;
    }

    public List<Interceptor<? extends Message>> getOutFaultInterceptors() {
        if (client != null) {
            return client.getOutFaultInterceptors();
        }
        return outFault;
    }

    public List<Interceptor<? extends Message>> getInFaultInterceptors() {
        if (client != null) {
            return client.getInFaultInterceptors();
        }
        return inFault;
    }

    public List<Interceptor<? extends Message>> getInInterceptors() {
        if (client != null) {
            return client.getInInterceptors();
        }
        return in;
    }

    public List<Interceptor<? extends Message>> getOutInterceptors() {
        if (client != null) {
            return client.getOutInterceptors();
        }
        return out;
    }

    public void setInInterceptors(List<Interceptor<? extends Message>> interceptors) {
        getInInterceptors().addAll(interceptors);
    }

    public void setInFaultInterceptors(List<Interceptor<? extends Message>> interceptors) {
        getInFaultInterceptors().addAll(interceptors);
    }

    public void setOutInterceptors(List<Interceptor<? extends Message>> interceptors) {
        getOutInterceptors().addAll(interceptors);
    }

    public void setOutFaultInterceptors(List<Interceptor<? extends Message>> interceptors) {
        getOutFaultInterceptors().addAll(interceptors);
    }

    public void setFeatures(List<? extends Feature> f) {
        features = CastUtils.cast(f);
    }
    public List<Feature> getFeatures() {
        return features;
    }

    public CallbackHandler getClaimsCallbackHandler() {
        return claimsCallbackHandler;
    }

    public void setClaimsCallbackHandler(CallbackHandler claimsCallbackHandler) {
        this.claimsCallbackHandler = claimsCallbackHandler;
    }

    protected static class STSResponse {
        private final DOMSource response;
        private final byte[] entropy;
        private final X509Certificate cert;
        private final Crypto crypto;
        private final Collection<Attachment> attachments;

        public STSResponse(DOMSource response, byte[] entropy) {
            this(response, entropy, null, null);
        }

        public STSResponse(DOMSource response, byte[] entropy, X509Certificate cert, Crypto crypto) {
            this(response, entropy, cert, crypto, null);
        }

        public STSResponse(DOMSource response, byte[] entropy, X509Certificate cert,
                           Crypto crypto, Collection<Attachment> attachments) {
            this.response = response;
            this.entropy = entropy;
            this.cert = cert;
            this.crypto = crypto;
            this.attachments = attachments;
        }


        public DOMSource getResponse() {
            return response;
        }

        public byte[] getEntropy() {
            return entropy;
        }

        public X509Certificate getCert() {
            return cert;
        }

        public Crypto getCrypto() {
            return crypto;
        }

        public Collection<Attachment> getAttachments() {
            return attachments;
        }
    }

    public String getWspNamespace() {
        return wspNamespace;
    }

    public void setWspNamespace(String wspNamespace) {
        this.wspNamespace = wspNamespace;
    }

    public X509Certificate getUseKeyCertificate() {
        return useKeyCertificate;
    }

    public void setUseKeyCertificate(X509Certificate useKeyCertificate) {
        this.useKeyCertificate = useKeyCertificate;
    }

    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }
}
