View Javadoc

1   /*
2    * The MIT License
3    *
4    * Original work sponsored and donated by National Board of e-Health (NSI), Denmark (http://www.nsi.dk)
5    *
6    * Copyright (C) 2011 National Board of e-Health (NSI), Denmark (http://www.nsi.dk)
7    *
8    * Permission is hereby granted, free of charge, to any person obtaining a copy of
9    * this software and associated documentation files (the "Software"), to deal in
10   * the Software without restriction, including without limitation the rights to
11   * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12   * of the Software, and to permit persons to whom the Software is furnished to do
13   * so, subject to the following conditions:
14   *
15   * The above copyright notice and this permission notice shall be included in all
16   * copies or substantial portions of the Software.
17   *
18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24   * SOFTWARE.
25   *
26   * $HeadURL$
27   * $Id$
28   */
29  
30  package dk.sosi.seal.model;
31  
32  import dk.sosi.seal.model.constants.*;
33  import dk.sosi.seal.modelbuilders.ModelBuildException;
34  import dk.sosi.seal.pki.CredentialVaultSignatureProvider;
35  import dk.sosi.seal.vault.CredentialVault;
36  import dk.sosi.seal.xml.XmlUtil;
37  import org.w3c.dom.Document;
38  import org.w3c.dom.Element;
39  import org.w3c.dom.NodeList;
40  import org.xml.sax.SAXException;
41  
42  import javax.xml.transform.dom.DOMSource;
43  import javax.xml.validation.Schema;
44  import javax.xml.validation.Validator;
45  import java.io.IOException;
46  import java.util.Date;
47  import java.util.LinkedList;
48  import java.util.List;
49  
50  /**
51   * @author $LastChangedBy:$ $LastChangedDate:$
52   * @version $Revision:$
53   */
54  public class LibertyRequestDOMEnhancer {
55  
56      private static Schema schema;
57  
58      private final CredentialVault credentialVault;
59      private final Document document;
60  
61      private String wsAddressingMessageID;
62      private String wsAddressingAction;
63      private String wsAddressingTo;
64      private IdentityToken identityToken;
65  
66      public LibertyRequestDOMEnhancer(CredentialVault credentialVault, Document document) {
67          if (credentialVault == null) throw new IllegalArgumentException("CredentialVault cannot be null");
68          if (document == null) throw new IllegalArgumentException("Document cannot be null");
69          this.credentialVault = credentialVault;
70          this.document = document;
71  
72          schemaValidate(document);
73      }
74  
75      /**
76       * Optional - generated otherwise, overwrites existing
77       *
78       * @param wsAddressingMessageID
79       */
80      public void setWSAddressingMessageID(String wsAddressingMessageID) {
81          if (isNullOrEmpty(wsAddressingMessageID)) {
82              throw new IllegalArgumentException("'wsAddressingMessageID' cannot be null or empty");
83          }
84          this.wsAddressingMessageID = wsAddressingMessageID;
85      }
86  
87      /**
88       * Required if not already present
89       *
90       * @param wsAddressingAction
91       */
92      public void setWSAddressingAction(String wsAddressingAction) {
93          if (isNullOrEmpty(wsAddressingAction)) {
94              throw new IllegalArgumentException("'wsAddressingAction' cannot be null or empty");
95          }
96          this.wsAddressingAction = wsAddressingAction;
97      }
98  
99      /**
100      * Optional
101      *
102      * @param wsAddressingTo
103      */
104     public void setWSAddressingTo(String wsAddressingTo) {
105         if (isNullOrEmpty(wsAddressingTo)) {
106             throw new IllegalArgumentException("'wsAddressingTo' cannot be null or empty");
107         }
108         this.wsAddressingTo = wsAddressingTo;
109     }
110 
111     /**
112      * Required, no checks are performed on the token
113      *
114      * @param identityToken
115      */
116     public void setIdentityToken(IdentityToken identityToken) {
117         if (identityToken == null) {
118             throw new IllegalArgumentException("'identityToken' cannot be null");
119         }
120         this.identityToken = identityToken;
121     }
122 
123 
124     public void enhanceAndSign() {
125         final Element header = XmlUtil.getFirstChildElementNS(document.getDocumentElement(), NameSpaces.SOAP_SCHEMA, SOAPTags.HEADER_UNPREFIXED);
126 
127         checkIdentityTokenSet();
128         checkForOldWSAddressingVersion(header);
129         checkForExistingWSSecurityHeader(header);
130 
131         ensureNameSpaceDeclarations(document);
132 
133         String messageIDWsuId = ensureWSAddressingMessageIDHeader(header);
134         String actionWsuId = ensureWSAddressingActionHeader(header);
135         String toWsuId = null;
136 
137         if (wsAddressingTo != null || hasWsAddressingToHeader(header)) {
138             toWsuId = ensureWSAddressingTo(header);
139         }
140 
141         String sbfWsuID = ensureLibertyFrameworkHeader(header);
142 
143         Element securityHeader = addWSSecurityHeader(header);
144         String securityWsuId = ensureIdAttribute(securityHeader, "security");
145 
146         String timestampWsuID = addWsuTimestamp(securityHeader);
147         String securityTokenReferenceID = addIdentityTokenAndCorrespondingSecurityTokenReference(securityHeader);
148         String bodyWsuId = ensureIdAttributeInBody(document);
149 
150         final List<SignatureConfiguration.Reference> references = new LinkedList<SignatureConfiguration.Reference>();
151         references.add(new SignatureConfiguration.Reference(messageIDWsuId, SignatureConfiguration.Type.DIRECT_REFERENCE));
152         references.add(new SignatureConfiguration.Reference(actionWsuId, SignatureConfiguration.Type.DIRECT_REFERENCE));
153         if (toWsuId != null) {
154             references.add(new SignatureConfiguration.Reference(toWsuId, SignatureConfiguration.Type.DIRECT_REFERENCE));
155         }
156         references.add(new SignatureConfiguration.Reference(sbfWsuID, SignatureConfiguration.Type.DIRECT_REFERENCE));
157         references.add(new SignatureConfiguration.Reference(timestampWsuID, SignatureConfiguration.Type.DIRECT_REFERENCE));
158         references.add(new SignatureConfiguration.Reference(securityTokenReferenceID, SignatureConfiguration.Type.SECURITY_TOKEN_REFERENCE));
159         references.add(new SignatureConfiguration.Reference(bodyWsuId, SignatureConfiguration.Type.DIRECT_REFERENCE));
160 
161         final SignatureConfiguration config = getSignatureConfiguration(securityWsuId, references);
162         SignatureUtil.sign(new CredentialVaultSignatureProvider(credentialVault), document, config);
163     }
164 
165     //To be able to override for unit-test purposes
166     protected SignatureConfiguration getSignatureConfiguration(String securityWsuId, List<SignatureConfiguration.Reference> references) {
167         return new SignatureConfiguration(references.toArray(new SignatureConfiguration.Reference[0]), securityWsuId, null);
168     }
169 
170     private void checkIdentityTokenSet() throws ModelBuildException {
171         if (identityToken == null) {
172             throw new ModelBuildException("IdentityToken must be set!");
173         }
174     }
175 
176     private void checkForOldWSAddressingVersion(Element header) throws ModelException {
177         final String message = "Document contains WS-Addressing headers in '" + NameSpaces.WSA_SCHEMA
178                 + "' namespace. Only WS-Addressing 1.0 (namespace '" + NameSpaces.WSA_1_0_SCHEMA + "') supported as required "
179                 + "by the Liberty Basic SOAP Binding is supported.";
180         checkForElementWithNamespace(header, NameSpaces.WSA_SCHEMA, message);
181     }
182 
183     private void checkForExistingWSSecurityHeader(Element header) throws ModelException {
184         checkForElementWithNamespace(header, NameSpaces.WSSE_SCHEMA, "Document already contains a WS-Security header!");
185     }
186 
187     private void checkForElementWithNamespace(Element element, String namesSpace, String errorMessage) throws ModelException {
188         final NodeList childNodes = element.getChildNodes();
189         for (int i = 0; i < childNodes.getLength(); i++) {
190             if (namesSpace.equals(childNodes.item(i).getNamespaceURI())) {
191                 throw new ModelException(errorMessage);
192             }
193         }
194     }
195 
196     private void ensureNameSpaceDeclarations(Document document) {
197         final Element root = document.getDocumentElement();
198         //TODO Fail if namespace declaration with same prefix but different URI exists?
199         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_WSA, NameSpaces.WSA_1_0_SCHEMA);
200         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_WSU, NameSpaces.WSU_SCHEMA);
201         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_SBF, NameSpaces.LIBERTY_SBF_SCHEMA);
202         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_SBFPROFILE, NameSpaces.LIBERTY_SBF_PROFILE_SCHEMA);
203         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_SAML, NameSpaces.SAML2ASSERTION_SCHEMA);
204         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_DS, NameSpaces.DSIG_SCHEMA);
205         root.setAttributeNS(NameSpaces.XMLNS_SCHEMA, NameSpaces.NS_XMLNS + ":" + NameSpaces.NS_WSSE, NameSpaces.WSSE_SCHEMA);
206     }
207 
208     private String ensureIdAttributeInBody(Document document) {
209         final Element body = XmlUtil.getFirstChildElementNS(document.getDocumentElement(), NameSpaces.SOAP_SCHEMA, SOAPTags.BODY_UNPREFIXED);
210         return ensureIdAttribute(body, "body");
211     }
212 
213     private String ensureWSAddressingMessageIDHeader(Element header) {
214         String value;
215         boolean overwrite;
216         if (wsAddressingMessageID != null) {
217             value = wsAddressingMessageID;
218             overwrite = true;
219         } else {
220             value = XmlUtil.generateUUID();
221             overwrite = false;
222         }
223         return ensureElement(header, NameSpaces.WSA_1_0_SCHEMA, WSATags.MESSAGE_ID, WSATags.MESSAGE_ID_PREFIXED, value, overwrite, "messageID");
224     }
225 
226     private String ensureWSAddressingActionHeader(Element header) {
227         final boolean overwrite = wsAddressingAction != null;
228         return ensureElement(header, NameSpaces.WSA_1_0_SCHEMA, WSATags.ACTION, WSATags.ACTION_PREFIXED, wsAddressingAction, overwrite, "action");
229     }
230 
231     private String ensureWSAddressingTo(Element header) {
232         final boolean overwrite = wsAddressingTo != null;
233         return ensureElement(header, NameSpaces.WSA_1_0_SCHEMA, WSATags.TO, WSATags.TO_PREFIXED, wsAddressingTo, overwrite, "to");
234     }
235 
236     private String ensureLibertyFrameworkHeader(Element header) {
237         Element framework = XmlUtil.getFirstChildElementNS(header, NameSpaces.LIBERTY_SBF_SCHEMA, LibertyTags.FRAMEWORK);
238         if (framework == null) {
239             framework = document.createElementNS(NameSpaces.LIBERTY_SBF_SCHEMA, LibertyTags.FRAMEWORK_PREFIXED);
240             header.appendChild(framework);
241         }
242         framework.setAttribute(LibertyAttributes.VERSION, LibertyValues.VERSION);
243         framework.setAttributeNS(NameSpaces.LIBERTY_SBF_PROFILE_SCHEMA, LibertyAttributes.PROFILE_PREFIXED, LibertyValues.PROFILE);
244         return ensureIdAttribute(framework, "sbf");
245     }
246 
247     private String addWsuTimestamp(Element securityHeader) {
248         final Element timestamp = document.createElementNS(NameSpaces.WSU_SCHEMA, WSUTags.TIMESTAMP_PREFIXED);
249         securityHeader.appendChild(timestamp);
250         final Element created = document.createElementNS(NameSpaces.WSU_SCHEMA, WSUTags.CREATED_PREFIXED);
251         //TODO allow to set created time explicitly?
252         created.setTextContent(XmlUtil.toXMLTimeStamp(new Date(), true));
253         timestamp.appendChild(created);
254         return ensureIdAttribute(timestamp, "ts");
255     }
256 
257     private String addIdentityTokenAndCorrespondingSecurityTokenReference(Element securityHeader) {
258         securityHeader.appendChild(document.importNode(identityToken.getDOM().getDocumentElement(), true));
259 
260         final Element securityTokenReference = document.createElementNS(NameSpaces.WSSE_SCHEMA, WSSETags.SECURITY_TOKEN_REFERENCE_PREFIXED);
261         securityTokenReference.setAttributeNS(NameSpaces.WSSE_1_1_SCHEMA, WSSE11Attributes.TOKEN_TYPE_PREFIXED, WSSEValues.SAML_TOKEN_TYPE);
262         securityHeader.appendChild(securityTokenReference);
263 
264         final Element keyIdentifier = document.createElementNS(NameSpaces.WSSE_SCHEMA, WSSETags.KEY_IDENTIFIER_PREFIXED);
265         keyIdentifier.setAttribute(WSSEAttributes.VALUE_TYPE, WSSEValues.VALUE_TYPE_SAML_ID);
266         keyIdentifier.setTextContent(identityToken.getID());
267         securityTokenReference.appendChild(keyIdentifier);
268         return ensureIdAttribute(securityTokenReference, "str");
269     }
270 
271 
272     private String ensureElement(Element parent, String nameSpaceURI, String tagName, String tagNamePrefixed, String value, boolean overwriteValue, String idValue) {
273         Element child = XmlUtil.getFirstChildElementNS(parent, nameSpaceURI, tagName);
274         if (child == null) {
275             if (isNullOrEmpty(value)) {
276                 throw new ModelBuildException("Required element '" + tagName + "' in namespace '" + nameSpaceURI + "' not present in document. " +
277                                                       "Failed to set it as no value has been provided for it.");
278             }
279             child = document.createElementNS(nameSpaceURI, tagNamePrefixed);
280             child.setTextContent(value);
281             parent.appendChild(child);
282         } else if (overwriteValue) {
283             child.setTextContent(value);
284         }
285         return ensureIdAttribute(child, idValue);
286     }
287 
288     private String ensureIdAttribute(Element element, String value) {
289         final String id = element.getAttributeNS(NameSpaces.WSU_SCHEMA, WSUAttributes.ID_UNPREFIXED);
290         if (isNullOrEmpty(id)) {
291             element.setAttributeNS(NameSpaces.WSU_SCHEMA, WSUAttributes.ID_PREFIXED, value);
292             return value;
293         } else {
294             return id;
295         }
296     }
297 
298     private boolean hasWsAddressingToHeader(Element header) {
299         return XmlUtil.getFirstChildElementNS(header, NameSpaces.WSA_1_0_SCHEMA, WSATags.TO) != null;
300     }
301 
302     private Element addWSSecurityHeader(Element header) {
303         final Element securityHeader = document.createElementNS(NameSpaces.WSSE_SCHEMA, WSSETags.SECURITY_PREFIXED);
304         securityHeader.setAttribute(WSSEAttributes.MUST_UNDERSTAND, "1");
305         header.appendChild(securityHeader);
306         return securityHeader;
307     }
308 
309     private void schemaValidate(Document envelope) {
310         try {
311             final Validator validator = getSchema().newValidator();
312             validator.validate(new DOMSource(envelope));
313         } catch (SAXException e) {
314             throw new ModelBuildException("Error validating SOAP message", e);
315         } catch (IOException e) {
316             throw new ModelBuildException("Error validating SOAP message", e);
317         }
318 
319     }
320 
321     private static synchronized Schema getSchema() throws SAXException {
322         if (schema == null) {
323             schema = SchemaUtil.loadSchema("/liberty/req/standard-soap.xsd");
324         }
325         return schema;
326     }
327 
328     private boolean isNullOrEmpty(String string) {
329         return string == null || string.equals("");
330     }
331 
332 }