View Javadoc

1   /*
2    * Copyright 2007 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.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.opensaml.Configuration;
24  import org.opensaml.common.SAMLObject;
25  import org.opensaml.common.SignableSAMLObject;
26  import org.opensaml.common.binding.SAMLMessageContext;
27  import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
28  import org.opensaml.saml2.core.StatusResponseType;
29  import org.opensaml.saml2.metadata.Endpoint;
30  import org.opensaml.util.URLBuilder;
31  import org.opensaml.ws.message.encoder.BaseMessageEncoder;
32  import org.opensaml.ws.message.encoder.MessageEncodingException;
33  import org.opensaml.xml.XMLObjectBuilder;
34  import org.opensaml.xml.io.Marshaller;
35  import org.opensaml.xml.io.MarshallingException;
36  import org.opensaml.xml.security.SecurityException;
37  import org.opensaml.xml.security.SecurityHelper;
38  import org.opensaml.xml.security.credential.Credential;
39  import org.opensaml.xml.signature.Signature;
40  import org.opensaml.xml.signature.SignatureException;
41  import org.opensaml.xml.signature.Signer;
42  import org.opensaml.xml.util.DatatypeHelper;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  /**
47   * Base class for SAML 2 message encoders.
48   */
49  public abstract class BaseSAML2MessageEncoder extends BaseMessageEncoder implements SAMLMessageEncoder {
50      
51      /** Class logger. */
52      private final Logger log = LoggerFactory.getLogger(BaseSAML2MessageEncoder.class);
53  
54      /** The list of schemes allowed to appear in URLs related to the encoded message. Defaults to 'http' and 'https'. */
55      private List<String> allowedURLSchemes;
56      
57      public BaseSAML2MessageEncoder(){
58          super();
59          setAllowedURLSchemes(new String[] { "http", "https" });
60      }
61      
62      /**
63       * Gets the unmodifiable list of schemes allowed to appear in URLs related to the encoded message.
64       * 
65       * @return list of URL schemes allowed to appear in a message
66       */
67      public List<String> getAllowedURLSchemes() {
68          return allowedURLSchemes;
69      }
70  
71      /**
72       * Sets the list of list of schemes allowed to appear in URLs related to the encoded message. Note, the appearance
73       * of schemes such as 'javascript' may open the system up to attacks (e.g. cross-site scripting attacks).
74       * 
75       * @param schemes URL schemes allowed to appear in a message
76       */
77      public void setAllowedURLSchemes(String[] schemes) {
78          if (schemes == null || schemes.length == 0) {
79              allowedURLSchemes = Collections.emptyList();
80          } else {
81              List<String> temp = new ArrayList<String>();
82              for (String scheme : schemes) {
83                  temp.add(scheme);
84              }
85              allowedURLSchemes = Collections.unmodifiableList(temp);
86          }
87      }
88      
89      /**
90       * Gets the response URL from the relying party endpoint. If the SAML message is a {@link StatusResponseType} and the relying
91       * party endpoint contains a response location then that location is returned otherwise the normal endpoint location
92       * is returned.
93       * 
94       * @param messageContext current message context
95       * 
96       * @return response URL from the relying party endpoint
97       * 
98       * @throws MessageEncodingException throw if no relying party endpoint is available
99       */
100     protected URLBuilder getEndpointURL(SAMLMessageContext messageContext) throws MessageEncodingException {
101         Endpoint endpoint = messageContext.getPeerEntityEndpoint();
102         if (endpoint == null) {
103             throw new MessageEncodingException("Endpoint for relying party was null.");
104         }
105         
106         URLBuilder urlBuilder;
107         if (messageContext.getOutboundMessage() instanceof StatusResponseType
108                 && !DatatypeHelper.isEmpty(endpoint.getResponseLocation())) {
109             urlBuilder = new URLBuilder(endpoint.getResponseLocation());
110         } else {
111             if (DatatypeHelper.isEmpty(endpoint.getLocation())) {
112                 throw new MessageEncodingException("Relying party endpoint location was null or empty.");
113             }
114             urlBuilder = new URLBuilder(endpoint.getLocation());
115         }
116         
117         if(!getAllowedURLSchemes().contains(urlBuilder.getScheme())){
118            throw new MessageEncodingException("Relying party endpoint used the untrusted URL scheme " + urlBuilder.getScheme()); 
119         }
120         return urlBuilder;
121     }
122 
123     /**
124      * Checks that the relay state is 80 bytes or less if it is not null.
125      * 
126      * @param relayState relay state to check
127      * 
128      * @return true if the relay state is not empty and is less than 80 bytes
129      */
130     protected boolean checkRelayState(String relayState) {
131         if (!DatatypeHelper.isEmpty(relayState)) {
132             if (relayState.getBytes().length > 80) {
133                 log.warn("Relay state exceeds 80 bytes, some application may not support this.");
134             }
135 
136             return true;
137         }
138 
139         return false;
140     }
141 
142     /**
143      * Sets the destination attribute on the outbound message if it is a {@link StatusResponseType} message.
144      * 
145      * @param outboundMessage outbound SAML message
146      * @param endpointURL destination endpoint
147      */
148     protected void setResponseDestination(SAMLObject outboundMessage, String endpointURL) {
149         if (outboundMessage instanceof StatusResponseType) {
150             ((StatusResponseType) outboundMessage).setDestination(endpointURL);
151         }
152     }
153 
154     /**
155      * Signs the given SAML message if it a {@link SignableSAMLObject} and this encoder has signing credentials.
156      * 
157      * @param messageContext current message context
158      * 
159      * @throws MessageEncodingException thrown if there is a problem marshalling or signing the outbound message
160      */
161     @SuppressWarnings("unchecked")
162     protected void signMessage(SAMLMessageContext messageContext) throws MessageEncodingException {
163         SAMLObject outboundSAML = messageContext.getOutboundSAMLMessage();
164         Credential signingCredential = messageContext.getOuboundSAMLMessageSigningCredential();
165 
166         if (outboundSAML instanceof SignableSAMLObject && signingCredential != null) {
167             SignableSAMLObject signableMessage = (SignableSAMLObject) outboundSAML;
168 
169             XMLObjectBuilder<Signature> signatureBuilder = Configuration.getBuilderFactory().getBuilder(
170                     Signature.DEFAULT_ELEMENT_NAME);
171             Signature signature = signatureBuilder.buildObject(Signature.DEFAULT_ELEMENT_NAME);
172             
173             signature.setSigningCredential(signingCredential);
174             try {
175                 //TODO pull SecurityConfiguration from SAMLMessageContext?  needs to be added
176                 //TODO pull binding-specific keyInfoGenName from encoder setting, etc?
177                 SecurityHelper.prepareSignatureParams(signature, signingCredential, null, null);
178             } catch (SecurityException e) {
179                 throw new MessageEncodingException("Error preparing signature for signing", e);
180             }
181             
182             signableMessage.setSignature(signature);
183 
184             try {
185                 Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(signableMessage);
186                 if (marshaller == null) {
187                     throw new MessageEncodingException("No marshaller registered for "
188                             + signableMessage.getElementQName() + ", unable to marshall in preperation for signing");
189                 }
190                 marshaller.marshall(signableMessage);
191 
192                 Signer.signObject(signature);
193             } catch (MarshallingException e) {
194                 log.error("Unable to marshall protocol message in preparation for signing", e);
195                 throw new MessageEncodingException("Unable to marshall protocol message in preparation for signing", e);
196             } catch (SignatureException e) {
197                 log.error("Unable to sign protocol message", e);
198                 throw new MessageEncodingException("Unable to sign protocol message", e);
199             }
200         }
201     }
202 }