org.springframework.security.saml.websso.AttributeQueryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.security.saml.websso.AttributeQueryImpl.java

Source

/* Copyright 2009 Vladimir Schafer
 *
 * 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.springframework.security.saml.websso;

import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;

import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.joda.time.DateTime;
import org.opensaml.Configuration;
import org.opensaml.common.SAMLException;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.SAMLObjectBuilder;
import org.opensaml.common.SAMLRuntimeException;
import org.opensaml.common.SAMLVersion;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AttributeQuery;
import org.opensaml.saml2.core.AttributeStatement;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.EncryptedAssertion;
import org.opensaml.saml2.core.EncryptedAttribute;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.RequestAbstractType;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.Subject;
import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
import org.opensaml.saml2.metadata.AttributeService;
import org.opensaml.saml2.metadata.Endpoint;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.security.MetadataCriteria;
import org.opensaml.ws.message.decoder.MessageDecodingException;
import org.opensaml.ws.message.encoder.MessageEncodingException;
import org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory;
import org.opensaml.ws.transport.http.HttpClientInTransport;
import org.opensaml.ws.transport.http.HttpClientOutTransport;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.XMLObjectBuilder;
import org.opensaml.xml.XMLObjectBuilderFactory;
import org.opensaml.xml.encryption.DecryptionException;
import org.opensaml.xml.schema.XSString;
import org.opensaml.xml.security.CriteriaSet;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.criteria.EntityIDCriteria;
import org.opensaml.xml.security.criteria.UsageCriteria;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.security.saml.context.SAMLContextProvider;
import org.springframework.security.saml.context.SAMLMessageContext;
import org.springframework.security.saml.metadata.ExtendedMetadata;
import org.springframework.security.saml.processor.SAMLProcessor;
import org.springframework.security.saml.storage.SAMLMessageStorage;
import org.springframework.security.saml.trust.X509KeyManager;
import org.springframework.security.saml.trust.X509TrustManager;
import org.springframework.security.saml.util.SAMLUtil;
import org.springframework.util.Assert;

/**
 * @author Amish Gandhi
 */
public class AttributeQueryImpl {

    /**
     * Class logger.
     */
    protected final static Logger log = LoggerFactory.getLogger(AttributeQueryImpl.class);

    XMLObjectBuilderFactory builderFactory;
    HttpClient httpClient;
    SAMLProcessor processor;
    protected SAMLContextProvider attrAuthContextProvider;
    SAMLMessageContext context;
    private AttributeQueryConsumer attributeQueryConsumer;

    /**
     * Sets entity responsible for populating local entity context data.
     *
     * @param contextProvider provider implementation
     */
    @Autowired
    public void setAttrAuthContextProvider(SAMLContextProvider attrAuthContextProvider) {
        Assert.notNull(attrAuthContextProvider, "Context provider can't be null");
        this.attrAuthContextProvider = attrAuthContextProvider;
    }

    @Autowired
    public void setAttributeQueryConsumer(AttributeQueryConsumer attributeQueryConsumer) {
        Assert.notNull(attributeQueryConsumer, "ssoConsumer can't be null");
        this.attributeQueryConsumer = attributeQueryConsumer;
    }

    public AttributeQueryImpl(HttpClient httpClient, SAMLProcessor processor) {
        builderFactory = Configuration.getBuilderFactory();
        this.httpClient = httpClient;
        //processor = new SAMLProcessorImpl(new HTTPSOAP11Binding(new StaticBasicParserPool()));
        this.processor = processor;
    }

    public void testAttributeQuery(HttpServletRequest request, HttpServletResponse response) {
        try {
            SAMLMessageContext context = attrAuthContextProvider.getLocalAndPeerEntity(request, response);

            createAttributeContext(context);
            getAttributeResponse(context);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * Uses HTTPClient to send and retrieve ArtifactMessages.
     *
     * @param endpointURI URI incoming artifactMessage is addressed to
     * @param context     context with filled communicationProfileId, outboundMessage, outboundSAMLMessage, peerEntityEndpoint, peerEntityId, peerEntityMetadata, peerEntityRole, peerEntityRoleMetadata
     * @throws SAMLException             error processing artifact messages
     * @throws MessageEncodingException  error sending artifactRequest
     * @throws MessageDecodingException  error retrieving artifactResponse
     * @throws MetadataProviderException error resolving metadata
     * @throws org.opensaml.xml.security.SecurityException
     *                                   invalid message signature
     */
    protected void getAttributeResponse(SAMLMessageContext context) throws SAMLException, MessageEncodingException,
            MessageDecodingException, MetadataProviderException, org.opensaml.xml.security.SecurityException {

        PostMethod postMethod = null;

        try {

            URI uri = new URI(context.getPeerEntityEndpoint().getLocation(), true, "UTF-8");
            postMethod = new PostMethod();
            postMethod.setPath(uri.getPath());

            HostConfiguration hc = getHostConfiguration(uri, context);

            HttpClientOutTransport clientOutTransport = new HttpClientOutTransport(postMethod);
            //HttpClientInTransport clientInTransport = new HttpClientInTransport(postMethod, "http://10.200.49.21:8080/spring-security-saml2-sample/saml/SSO/alias/defaultAlias");
            HttpClientInTransport clientInTransport = new HttpClientInTransport(postMethod, "");

            context.setInboundMessageTransport(clientInTransport);
            context.setOutboundMessageTransport(clientOutTransport);

            // Send artifact retrieve message
            boolean signMessage = true;
            processor.sendMessage(context, signMessage, SAMLConstants.SAML2_SOAP11_BINDING_URI);

            log.debug("Sending Attribute message to {}", uri);
            int responseCode = httpClient.executeMethod(hc, postMethod);
            if (responseCode != 200) {
                String responseBody = postMethod.getResponseBodyAsString();
                throw new MessageDecodingException(
                        "Problem communicating with Attribute Query service, received response " + responseCode
                                + ", body " + responseBody);
            }

            // Decode artifact response message.
            processor.retrieveMessage(context, SAMLConstants.SAML2_SOAP11_BINDING_URI);
            List<Attribute> attributes = attributeQueryConsumer.processAttributeQueryResponse(context);

        } catch (Exception e) {
            e.printStackTrace();
            throw new MessageDecodingException("Error when sending request to artifact resolution service.", e);

        } finally {

            if (postMethod != null) {
                postMethod.releaseConnection();
            }

        }

    }

    /**
     * Initializes SSO by creating AuthnRequest assertion and sending it to the IDP using the default binding.
     * Default IDP is used to send the request.
     *
     *
     * @param options        values specified by caller to customize format of sent request
     * @throws SAMLException             error initializing SSO
     * @throws SAMLRuntimeException in case context doesn't contain required entities or contains invalid data
     * @throws MetadataProviderException error retrieving needed metadata
     * @throws MessageEncodingException  error forming SAML message
     */
    public void createAttributeContext(SAMLMessageContext context)
            throws SAMLException, MetadataProviderException, MessageEncodingException {

        // Verify we deal with a local SP
        if (!AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME.equals(context.getLocalEntityRole())) {
            throw new SAMLException("AttributeQuery can only be initialized for local SP, but localEntityRole is: "
                    + context.getLocalEntityRole());
        }

        // Load the entities from the context
        AttributeAuthorityDescriptor idpattrDescriptor = (AttributeAuthorityDescriptor) context
                .getPeerEntityRoleMetadata();
        ExtendedMetadata idpExtendedMetadata = context.getPeerExtendedMetadata();

        if (idpattrDescriptor == null || idpExtendedMetadata == null) {
            throw new SAMLException(
                    "AttributeAuthorityDescriptor, AttributeAuthorityDescriptor or IDPExtendedMetadata are not present in the SAMLContext");
        }

        AttributeService attrService = getAttributeService(idpattrDescriptor);
        AttributeQuery query = getAttributeRequest(context);

        context.setCommunicationProfileId("urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
        context.setOutboundMessage(query);
        context.setOutboundSAMLMessage(query);
        context.setPeerEntityEndpoint(attrService);
        context.setPeerEntityId(idpattrDescriptor.getID());
        context.setPeerEntityRoleMetadata(idpattrDescriptor);
        context.setPeerExtendedMetadata(idpExtendedMetadata);

        SAMLMessageStorage messageStorage = context.getMessageStorage();
        if (messageStorage != null) {
            messageStorage.storeMessage(query.getID(), query);
        }

    }

    /**
     * Method determines SingleSignOn service (and thus binding) to be used to deliver AuthnRequest to the IDP.
     * When binding is specified in the WebSSOProfileOptions it is honored. Otherwise first suitable binding is used.
     *
     * @param options          user supplied preferences, binding attribute is used
     * @param idpattrDescriptor idp
     * @param spDescriptor     sp
     * @return service to send message to
     * @throws MetadataProviderException in case binding from the options is invalid or not found or when no default service can be found
     */
    protected AttributeService getAttributeService(AttributeAuthorityDescriptor idpattrDescriptor)
            throws MetadataProviderException {

        // Find the endpoint
        List<AttributeService> services = idpattrDescriptor.getAttributeServices();
        //TODO Return the binding based on option selected
        for (AttributeService service : services) {
            // Use as a default
            return service;
        }

        return null;

    }

    protected AttributeQuery getAttributeRequest(SAMLMessageContext context)
            throws SAMLException, MetadataProviderException {

        SAMLObjectBuilder<AttributeQuery> builder = (SAMLObjectBuilder<AttributeQuery>) builderFactory
                .getBuilder(AttributeQuery.DEFAULT_ELEMENT_NAME);
        AttributeQuery request = builder.buildObject();

        SAMLObjectBuilder<Attribute> attributeBuilder = (SAMLObjectBuilder<Attribute>) builderFactory
                .getBuilder(Attribute.DEFAULT_ELEMENT_NAME);

        XMLObjectBuilder<XSString> xsstringBuilder = (XMLObjectBuilder<XSString>) builderFactory
                .getBuilder(XSString.TYPE_NAME);

        //      XSString xsstring = xsstringBuilder
        //      xsstring.se
        List<Attribute> attributes = request.getAttributes();

        Attribute attribute = attributeBuilder.buildObject();
        attribute.setFriendlyName("mail");
        attribute.setName("urn:oid:0.9.2342.19200300.100.1.3");
        attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri");

        List<XMLObject> values = attribute.getAttributeValues();
        QName qName = new QName("string", "http://www.w3.org/2001/XMLSchema");
        XSString xsstring = xsstringBuilder.buildObject(qName);
        xsstring.setValue("ccm1001@atseng.com");
        values.add(xsstring);

        //attributes.add(attribute);

        SAMLObjectBuilder<Subject> subjectBuilder = (SAMLObjectBuilder<Subject>) builderFactory
                .getBuilder(Subject.DEFAULT_ELEMENT_NAME);
        Subject subject = subjectBuilder.buildObject();

        SAMLObjectBuilder<NameID> nameIDbuilder = (SAMLObjectBuilder<NameID>) builderFactory
                .getBuilder(NameID.DEFAULT_ELEMENT_NAME);
        NameID nameID = nameIDbuilder.buildObject();
        nameID.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:principalName");
        nameID.setValue("ccmsso1");

        subject.setNameID(nameID);

        request.setSubject(subject);

        request.setVersion(SAMLVersion.VERSION_20);

        buildCommonAttributes(context.getLocalEntityId(), request);

        return request;

    }

    /**
     * Fills the request with version, issue instants and destination data.
     *
     * @param localEntityId entityId of the local party acting as message issuer
     * @param request       request to be filled
     * @param service       service to use as destination for the request
     */
    protected void buildCommonAttributes(String localEntityId, RequestAbstractType request) {

        request.setID(generateID());
        request.setIssuer(getIssuer(localEntityId));
        request.setVersion(SAMLVersion.VERSION_20);
        request.setIssueInstant(new DateTime());

        /*        if (service != null) {
        // Service is now known when we do not know which IDP will be used
        request.setDestination(service.getLocation());
                }*/

    }

    protected Issuer getIssuer(String localEntityId) {
        SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) builderFactory
                .getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
        Issuer issuer = issuerBuilder.buildObject();
        issuer.setValue(localEntityId);
        return issuer;
    }

    /**
     * Generates random ID to be used as Request/Response ID.
     *
     * @return random ID
     */
    protected String generateID() {
        Random r = new Random();
        return 'a' + Long.toString(Math.abs(r.nextLong()), 20) + Long.toString(Math.abs(r.nextLong()), 20);
    }

    /**
     * SAML-Core 2218, Specifies that returned subject identifier should be returned in the namespace of the given SP.
     *
     * @return by default returns null
     */
    protected String getSPNameQualifier() {
        return null;
    }

    /**
     * Method is expected to return binding used to transfer messages to this endpoint. For some profiles the
     * binding attribute in the metadata contains the profile name, method correctly parses the real binding
     * in these situations.
     *
     * @param endpoint endpoint
     * @return binding
     */
    protected String getEndpointBinding(Endpoint endpoint) {
        return SAMLUtil.getBindingForEndpoint(endpoint);
    }

    /**
     * Method is expected to determine hostConfiguration used to send request to the server by back-channel. Configuration
     * should contain URI of the host and used protocol including all security settings.
     * <p/>
     * Default implementation uses either default http protocol for non-SSL requests or constructs a separate
     * TrustManager using trust engine specified in the SAMLMessageContext - based either on MetaIOP (certificates
     * obtained from Metadata and ExtendedMetadata are trusted) or PKIX (certificates from metadata and ExtendedMetadata
     * including specified trust anchors are trusted and verified using PKIX).
     * <p/>
     * Used trust engine can be customized as part of the SAMLContextProvider used to process this request.
     * <p/>
     * Default values for the HostConfiguration are cloned from the HTTPClient set in this instance, when there are
     * no defaults available a new object is created.
     *
     * @param uri uri the request should be sent to
     * @param context context including the peer address
     * @return host configuration
     * @throws MessageEncodingException in case peer URI can't be parsed
     */
    protected HostConfiguration getHostConfiguration(URI uri, SAMLMessageContext context)
            throws MessageEncodingException {

        try {

            HostConfiguration hc = httpClient.getHostConfiguration();

            if (hc != null) {
                // Clone configuration from the HTTP Client object
                hc = new HostConfiguration(hc);
            } else {
                // Create brand new configuration when there are no defaults
                hc = new HostConfiguration();
            }

            if (uri.getScheme().equalsIgnoreCase("http")) {

                log.debug("Using HTTP configuration");
                hc.setHost(uri);

            } else {

                log.debug("Using HTTPS configuration");

                CriteriaSet criteriaSet = new CriteriaSet();
                criteriaSet.add(new EntityIDCriteria(context.getPeerEntityId()));
                criteriaSet
                        .add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
                criteriaSet.add(new UsageCriteria(UsageType.UNSPECIFIED));

                X509TrustManager trustManager = new X509TrustManager(criteriaSet, context.getLocalSSLTrustEngine());
                X509KeyManager manager = new X509KeyManager(context.getLocalSSLCredential());
                Protocol protocol = new Protocol("https",
                        (ProtocolSocketFactory) new TLSProtocolSocketFactory(manager, trustManager), 443);
                hc.setHost(uri.getHost(), uri.getPort(), protocol);

            }

            return hc;

        } catch (URIException e) {
            throw new MessageEncodingException("Error parsing remote location URI", e);
        }

    }

}