View Javadoc

1   /*
2    * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.saml2.binding.encoding;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.UnsupportedEncodingException;
22  import java.util.List;
23  import java.util.zip.Deflater;
24  import java.util.zip.DeflaterOutputStream;
25  
26  import org.opensaml.common.SAMLObject;
27  import org.opensaml.common.SignableSAMLObject;
28  import org.opensaml.common.binding.SAMLMessageContext;
29  import org.opensaml.common.xml.SAMLConstants;
30  import org.opensaml.saml2.core.RequestAbstractType;
31  import org.opensaml.saml2.core.StatusResponseType;
32  import org.opensaml.util.URLBuilder;
33  import org.opensaml.ws.message.MessageContext;
34  import org.opensaml.ws.message.encoder.MessageEncodingException;
35  import org.opensaml.ws.transport.http.HTTPOutTransport;
36  import org.opensaml.ws.transport.http.HTTPTransportUtils;
37  import org.opensaml.xml.Configuration;
38  import org.opensaml.xml.security.SecurityConfiguration;
39  import org.opensaml.xml.security.SecurityException;
40  import org.opensaml.xml.security.SecurityHelper;
41  import org.opensaml.xml.security.SigningUtil;
42  import org.opensaml.xml.security.credential.Credential;
43  import org.opensaml.xml.util.Base64;
44  import org.opensaml.xml.util.Pair;
45  import org.opensaml.xml.util.XMLHelper;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  /**
50   * SAML 2.0 HTTP Redirect encoder using the DEFLATE encoding method.
51   * 
52   * This encoder only supports DEFLATE compression and DSA-SHA1 and RSA-SHA1 signatures.
53   */
54  public class HTTPRedirectDeflateEncoder extends BaseSAML2MessageEncoder {
55  
56      /** Class logger. */
57      private final Logger log = LoggerFactory.getLogger(HTTPRedirectDeflateEncoder.class);
58  
59      /** Constructor. */
60      public HTTPRedirectDeflateEncoder() {
61          super();
62      }
63  
64      /** {@inheritDoc} */
65      public String getBindingURI() {
66          return SAMLConstants.SAML2_REDIRECT_BINDING_URI;
67      }
68      
69      /** {@inheritDoc} */
70      public boolean providesMessageConfidentiality(MessageContext messageContext) throws MessageEncodingException {
71          return false;
72      }
73  
74      /** {@inheritDoc} */
75      public boolean providesMessageIntegrity(MessageContext messageContext) throws MessageEncodingException {
76          return false;
77      }
78  
79      /** {@inheritDoc} */
80      protected void doEncode(MessageContext messageContext) throws MessageEncodingException {
81          if (!(messageContext instanceof SAMLMessageContext)) {
82              log.error("Invalid message context type, this encoder only support SAMLMessageContext");
83              throw new MessageEncodingException(
84                      "Invalid message context type, this encoder only support SAMLMessageContext");
85          }
86  
87          if (!(messageContext.getOutboundMessageTransport() instanceof HTTPOutTransport)) {
88              log.error("Invalid outbound message transport type, this encoder only support HTTPOutTransport");
89              throw new MessageEncodingException(
90                      "Invalid outbound message transport type, this encoder only support HTTPOutTransport");
91          }
92  
93          SAMLMessageContext samlMsgCtx = (SAMLMessageContext) messageContext;
94  
95          String endpointURL = getEndpointURL(samlMsgCtx).buildURL();
96  
97          setResponseDestination(samlMsgCtx.getOutboundSAMLMessage(), endpointURL);
98  
99          removeSignature(samlMsgCtx);
100 
101         String encodedMessage = deflateAndBase64Encode(samlMsgCtx.getOutboundSAMLMessage());
102 
103         String redirectURL = buildRedirectURL(samlMsgCtx, endpointURL, encodedMessage);
104 
105         HTTPOutTransport out = (HTTPOutTransport) messageContext.getOutboundMessageTransport();
106         HTTPTransportUtils.addNoCacheHeaders(out);
107         HTTPTransportUtils.setUTF8Encoding(out);
108 
109         out.sendRedirect(redirectURL);
110     }
111 
112     /**
113      * Removes the signature from the protocol message.
114      * 
115      * @param messageContext current message context
116      */
117     protected void removeSignature(SAMLMessageContext messageContext) {
118         SignableSAMLObject message = (SignableSAMLObject) messageContext.getOutboundSAMLMessage();
119         if (message.isSigned()) {
120             log.debug("Removing SAML protocol message signature");
121             message.setSignature(null);
122         }
123     }
124 
125     /**
126      * DEFLATE (RFC1951) compresses the given SAML message.
127      * 
128      * @param message SAML message
129      * 
130      * @return DEFLATE compressed message
131      * 
132      * @throws MessageEncodingException thrown if there is a problem compressing the message
133      */
134     protected String deflateAndBase64Encode(SAMLObject message) throws MessageEncodingException {
135         log.debug("Deflating and Base64 encoding SAML message");
136         try {
137             String messageStr = XMLHelper.nodeToString(marshallMessage(message));
138 
139             ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
140             Deflater deflater = new Deflater(Deflater.DEFLATED, true);
141             DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater);
142             deflaterStream.write(messageStr.getBytes("UTF-8"));
143             deflaterStream.finish();
144 
145             return Base64.encodeBytes(bytesOut.toByteArray(), Base64.DONT_BREAK_LINES);
146         } catch (IOException e) {
147             throw new MessageEncodingException("Unable to DEFLATE and Base64 encode SAML message", e);
148         }
149     }
150 
151     /**
152      * Builds the URL to redirect the client to.
153      * 
154      * @param messagesContext current message context
155      * @param endpointURL endpoint URL to send encoded message to
156      * @param message Deflated and Base64 encoded message
157      * 
158      * @return URL to redirect client to
159      * 
160      * @throws MessageEncodingException thrown if the SAML message is neither a RequestAbstractType or Response
161      */
162     protected String buildRedirectURL(SAMLMessageContext messagesContext, String endpointURL, String message)
163             throws MessageEncodingException {
164         log.debug("Building URL to redirect client to");
165         URLBuilder urlBuilder = new URLBuilder(endpointURL);
166 
167         List<Pair<String, String>> queryParams = urlBuilder.getQueryParams();
168         queryParams.clear();
169 
170         if (messagesContext.getOutboundSAMLMessage() instanceof RequestAbstractType) {
171             queryParams.add(new Pair<String, String>("SAMLRequest", message));
172         } else if (messagesContext.getOutboundSAMLMessage() instanceof StatusResponseType) {
173             queryParams.add(new Pair<String, String>("SAMLResponse", message));
174         } else {
175             throw new MessageEncodingException(
176                     "SAML message is neither a SAML RequestAbstractType or StatusResponseType");
177         }
178 
179         String relayState = messagesContext.getRelayState();
180         if (checkRelayState(relayState)) {
181             queryParams.add(new Pair<String, String>("RelayState", relayState));
182         }
183 
184         Credential signingCredential = messagesContext.getOuboundSAMLMessageSigningCredential();
185         if (signingCredential != null) {
186             // TODO pull SecurityConfiguration from SAMLMessageContext? needs to be added
187             String sigAlgURI = getSignatureAlgorithmURI(signingCredential, null);
188             Pair<String, String> sigAlg = new Pair<String, String>("SigAlg", sigAlgURI);
189             queryParams.add(sigAlg);
190             String sigMaterial = urlBuilder.buildQueryString();
191 
192             queryParams.add(new Pair<String, String>("Signature", generateSignature(signingCredential, sigAlgURI,
193                     sigMaterial)));
194         }
195 
196         return urlBuilder.buildURL();
197     }
198 
199     /**
200      * Gets the signature algorithm URI to use with the given signing credential.
201      * 
202      * @param credential the credential that will be used to sign the message
203      * @param config the SecurityConfiguration to use (may be null)
204      * 
205      * @return signature algorithm to use with the given signing credential
206      * 
207      * @throws MessageEncodingException thrown if the algorithm URI could not be derived from the supplied credential
208      */
209     protected String getSignatureAlgorithmURI(Credential credential, SecurityConfiguration config)
210             throws MessageEncodingException {
211 
212         SecurityConfiguration secConfig;
213         if (config != null) {
214             secConfig = config;
215         } else {
216             secConfig = Configuration.getGlobalSecurityConfiguration();
217         }
218 
219         String signAlgo = secConfig.getSignatureAlgorithmURI(credential);
220 
221         if (signAlgo == null) {
222             throw new MessageEncodingException("The signing credential's algorithm URI could not be derived");
223         }
224 
225         return signAlgo;
226     }
227 
228     /**
229      * Generates the signature over the query string.
230      * 
231      * @param signingCredential credential that will be used to sign query string
232      * @param algorithmURI algorithm URI of the signing credential
233      * @param queryString query string to be signed
234      * 
235      * @return base64 encoded signature of query string
236      * 
237      * @throws MessageEncodingException there is an error computing the signature
238      */
239     protected String generateSignature(Credential signingCredential, String algorithmURI, String queryString)
240             throws MessageEncodingException {
241 
242         log.debug(String.format("Generating signature with key type '%s', algorithm URI '%s' over query string '%s'",
243                 SecurityHelper.extractSigningKey(signingCredential).getAlgorithm(), algorithmURI, queryString));
244 
245         String b64Signature = null;
246         try {
247             byte[] rawSignature = SigningUtil.signWithURI(signingCredential, algorithmURI, queryString
248                     .getBytes("UTF-8"));
249             b64Signature = Base64.encodeBytes(rawSignature, Base64.DONT_BREAK_LINES);
250             log.debug("Generated digital signature value (base64-encoded) {}", b64Signature);
251         } catch (SecurityException e) {
252             log.error("Error during URL signing process", e);
253             throw new MessageEncodingException("Unable to sign URL query string", e);
254         } catch (UnsupportedEncodingException e) {
255             // UTF-8 encoding is required to be supported by all JVMs
256         }
257 
258         return b64Signature;
259     }
260 }