All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the University Corporation for Advanced Internet Development,
 * Inc. (UCAID) under one or more contributor license agreements.  See the
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID 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.opensaml.saml.saml2.binding.encoding.impl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletResponse;

import net.shibboleth.utilities.java.support.codec.Base64Support;
import net.shibboleth.utilities.java.support.codec.EncodingException;
import net.shibboleth.utilities.java.support.collection.Pair;
import net.shibboleth.utilities.java.support.net.HttpServletSupport;
import net.shibboleth.utilities.java.support.net.URLBuilder;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;

import org.opensaml.messaging.context.MessageContext;
import org.opensaml.messaging.encoder.MessageEncodingException;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.common.binding.SAMLBindingSupport;
import org.opensaml.saml.common.messaging.SAMLMessageSecuritySupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

/**
 * SAML 2.0 HTTP Redirect encoder using the DEFLATE encoding method.
 * 
 * This encoder only supports DEFLATE compression.
 */
public class HTTPRedirectDeflateEncoder extends BaseSAML2MessageEncoder {
    
    /** Params which are disallowed from appearing in the input endpoint URL. */
    private static final Set DISALLOWED_ENDPOINT_QUERY_PARAMS = 
            Sets.newHashSet("SAMLEncoding", "SAMLRequest", "SAMLResponse", "RelayState", "SigAlg", "Signature");

    /** Class logger. */
    private final Logger log = LoggerFactory.getLogger(HTTPRedirectDeflateEncoder.class);

    /** Constructor. */
    public HTTPRedirectDeflateEncoder() {
        
    }

    /** {@inheritDoc} */
    public String getBindingURI() {
        return SAMLConstants.SAML2_REDIRECT_BINDING_URI;
    }

    /** {@inheritDoc} */
    protected void doEncode() throws MessageEncodingException {
        final MessageContext messageContext = getMessageContext();
        final Object outboundMessage = messageContext.getMessage();
        if (outboundMessage == null || !(outboundMessage instanceof SAMLObject)) {
            throw new MessageEncodingException("No outbound SAML message contained in message context");
        }

        final String endpointURL = getEndpointURL(messageContext).toString();

        removeSignature((SAMLObject) outboundMessage);

        final String encodedMessage = deflateAndBase64Encode((SAMLObject) outboundMessage);

        final String redirectURL = buildRedirectURL(messageContext, endpointURL, encodedMessage);

        final HttpServletResponse response = getHttpServletResponse();
        HttpServletSupport.addNoCacheHeaders(response);
        HttpServletSupport.setUTF8Encoding(response);

        try {
            response.sendRedirect(redirectURL);
        } catch (final IOException e) {
            throw new MessageEncodingException("Problem sending HTTP redirect", e);
        }
    }

    /**
     * Removes the signature from the protocol message.
     * 
     * @param message current message context
     */
    protected void removeSignature(final SAMLObject message) {
        if (message instanceof SignableSAMLObject) {
            final SignableSAMLObject signableMessage = (SignableSAMLObject) message;
            if (signableMessage.isSigned()) {
                log.debug("Removing SAML protocol message signature");
                signableMessage.setSignature(null);
            }
        }
    }

    /**
     * DEFLATE (RFC1951) compresses the given SAML message.
     * 
     * @param message SAML message
     * 
     * @return DEFLATE compressed message
     * 
     * @throws MessageEncodingException thrown if there is a problem compressing the message
     */
    protected String deflateAndBase64Encode(final SAMLObject message) throws MessageEncodingException {
        log.debug("Deflating and Base64 encoding SAML message");
        try {
            final String messageStr = SerializeSupport.nodeToString(marshallMessage(message));

            try (final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
                    final DeflaterOutputStream deflaterStream =
                            new NoWrapAutoEndDeflaterOutputStream(bytesOut, Deflater.DEFLATED)) {

                deflaterStream.write(messageStr.getBytes("UTF-8"));
                deflaterStream.finish();

                return Base64Support.encode(bytesOut.toByteArray(), Base64Support.UNCHUNKED);
            }            
        } catch (final IOException | EncodingException e) {
            throw new MessageEncodingException("Unable to DEFLATE and Base64 encode SAML message", e);
        } 
    }

    /**
     * Builds the URL to redirect the client to.
     * 
     * @param messageContext current message context
     * @param endpoint endpoint URL to send encoded message to
     * @param message Deflated and Base64 encoded message
     * 
     * @return URL to redirect client to
     * 
     * @throws MessageEncodingException thrown if the SAML message is neither a RequestAbstractType or Response
     */
    protected String buildRedirectURL(final MessageContext messageContext, final String endpoint, final String message)
            throws MessageEncodingException {
        log.debug("Building URL to redirect client to");
        
        URLBuilder urlBuilder = null;
        try {
            urlBuilder = new URLBuilder(endpoint);
        } catch (final MalformedURLException e) {
            throw new MessageEncodingException("Endpoint URL " + endpoint + " is not a valid URL", e);
        }

        final List> queryParams = urlBuilder.getQueryParams();
        removeDisallowedQueryParams(queryParams);
        
        // This is a copy of any existing allowed params that were preserved.  Note that they will not be signed.
        final List> originalParams = new ArrayList<>(queryParams);

        // We clear here so that existing params will not be signed, but can still use the URLBuilder#buildQueryString()
        // to build the string that will potentially be signed later. Add originalParms back in later.
        queryParams.clear();

        final SAMLObject outboundMessage = (SAMLObject) messageContext.getMessage();

        if (outboundMessage instanceof RequestAbstractType) {
            queryParams.add(new Pair<>("SAMLRequest", message));
        } else if (outboundMessage instanceof StatusResponseType) {
            queryParams.add(new Pair<>("SAMLResponse", message));
        } else {
            throw new MessageEncodingException(
                    "SAML message is neither a SAML RequestAbstractType or StatusResponseType");
        }

        final String relayState = SAMLBindingSupport.getRelayState(messageContext);
        if (SAMLBindingSupport.checkRelayState(relayState)) {
            queryParams.add(new Pair<>("RelayState", relayState));
        }

        final SignatureSigningParameters signingParameters = 
                SAMLMessageSecuritySupport.getContextSigningParameters(messageContext);
        if (signingParameters != null && signingParameters.getSigningCredential() != null) {
            final String sigAlgURI =  getSignatureAlgorithmURI(signingParameters);
            final Pair sigAlg = new Pair<>("SigAlg", sigAlgURI);
            queryParams.add(sigAlg);
            final String sigMaterial = urlBuilder.buildQueryString();

            queryParams.add(new Pair<>("Signature", generateSignature(
                    signingParameters.getSigningCredential(), sigAlgURI, sigMaterial)));

            // Add original params to the beginning of the list preserving their original order.
            if (!originalParams.isEmpty()) {
                for (final Pair param : Lists.reverse(originalParams)) {
                    queryParams.add(0, param);
                }
            }

        } else {
            log.debug("No signing credential was supplied, skipping HTTP-Redirect DEFLATE signing");
            queryParams.addAll(originalParams);
        }
        
        return urlBuilder.buildURL();
    }

    /**
     * Remove disallowed query params from the supplied list.
     * 
     * @param queryParams the list of query params on which to operate
     */
    protected void removeDisallowedQueryParams(final @Nonnull List> queryParams) {
        final Iterator> iter = queryParams.iterator();
        while (iter.hasNext()) {
            final String paramName = StringSupport.trimOrNull(iter.next().getFirst());
            if (DISALLOWED_ENDPOINT_QUERY_PARAMS.contains(paramName)) {
                log.debug("Removing disallowed query param '{}' from endpoint URL", paramName);
                iter.remove();
            }
        }
    }

    /**
     * Gets the signature algorithm URI to use.
     * 
     * @param signingParameters the signing parameters to use
     * 
     * @return signature algorithm to use with the associated signing credential
     * 
     * @throws MessageEncodingException thrown if the algorithm URI is not supplied explicitly and 
     *          could not be derived from the supplied credential
     */
    protected String getSignatureAlgorithmURI(final SignatureSigningParameters signingParameters)
            throws MessageEncodingException {
        
        if (signingParameters.getSignatureAlgorithm() != null) {
            return signingParameters.getSignatureAlgorithm();
        }

        throw new MessageEncodingException("The signing algorithm URI could not be determined");
    }

    /**
     * Generates the signature over the query string.
     * 
     * @param signingCredential credential that will be used to sign query string
     * @param algorithmURI algorithm URI of the signing credential
     * @param queryString query string to be signed
     * 
     * @return base64 encoded signature of query string
     * 
     * @throws MessageEncodingException there is an error computing the signature
     */
    protected String generateSignature(final Credential signingCredential, final String algorithmURI,
            final String queryString)
            throws MessageEncodingException {

        log.debug(String.format("Generating signature with key type '%s', algorithm URI '%s' over query string '%s'",
                CredentialSupport.extractSigningKey(signingCredential).getAlgorithm(), algorithmURI, queryString));

        String b64Signature = null;
        try {
            final byte[] rawSignature =
                    XMLSigningUtil.signWithURI(signingCredential, algorithmURI, queryString.getBytes("UTF-8"));
            b64Signature = Base64Support.encode(rawSignature, Base64Support.UNCHUNKED);
            log.debug("Generated digital signature value (base64-encoded) {}", b64Signature);
        } catch (final SecurityException e) {
            log.error("Error during URL signing process: {}", e.getMessage());
            throw new MessageEncodingException("Unable to sign URL query string", e);
        } catch (final UnsupportedEncodingException e) {
            // UTF-8 encoding is required to be supported by all JVMs
        } catch (final EncodingException e) {
            log.error("Error during URL signing process: {}", e.getMessage());
            throw new MessageEncodingException("Unable to base64 encode signature of URL query string", e);
        }

        return b64Signature;
    }

    /** A subclass of {@link DeflaterOutputStream} which defaults in a no-wrap {@link Deflater} instance and
     * closes it when the stream is closed.
     */
    private class NoWrapAutoEndDeflaterOutputStream extends DeflaterOutputStream {

        /**
         * Creates a new output stream with a default no-wrap compressor and buffer size,
         * and the specified compression level.
         *
         * @param os the output stream
         * @param level the compression level (0-9)
         */
        public NoWrapAutoEndDeflaterOutputStream(final OutputStream os, final int level) {
            super(os, new Deflater(level, true));
        }

        /** {@inheritDoc} */
        public void close() throws IOException {
            if (def != null) {
                def.end();
            }
            super.close();
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy