View Javadoc

1   /**
2    * Copyright 2010 The Kuali Foundation Licensed under the
3    * Educational Community License, Version 2.0 (the "License"); you may
4    * not use this file except in compliance with the License. You may
5    * obtain a copy of the License at
6    *
7    * http://www.osedu.org/licenses/ECL-2.0
8    *
9    * Unless required by applicable law or agreed to in writing,
10   * software distributed under the License is distributed on an "AS IS"
11   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing
13   * permissions and limitations under the License.
14   */
15  
16  package org.kuali.student.security.cxf.interceptors;
17  
18  import java.util.*;
19  import java.util.logging.Level;
20  import java.util.logging.Logger;
21  
22  import javax.xml.soap.SOAPElement;
23  import javax.xml.soap.SOAPEnvelope;
24  import javax.xml.soap.SOAPException;
25  import javax.xml.soap.SOAPFactory;
26  import javax.xml.soap.SOAPHeader;
27  import javax.xml.soap.SOAPMessage;
28  import javax.xml.soap.SOAPPart;
29  
30  import org.apache.cxf.binding.soap.SoapFault;
31  import org.apache.cxf.binding.soap.SoapMessage;
32  import org.apache.cxf.binding.soap.SoapVersion;
33  import org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor;
34  import org.apache.cxf.common.i18n.Message;
35  import org.apache.cxf.common.logging.LogUtils;
36  import org.apache.cxf.interceptor.Fault;
37  import org.apache.cxf.phase.Phase;
38  import org.apache.cxf.phase.PhaseInterceptor;
39  import org.apache.cxf.ws.security.wss4j.AbstractWSS4JInterceptor;
40  import org.apache.ws.security.WSConstants;
41  import org.apache.ws.security.WSEncryptionPart;
42  import org.apache.ws.security.WSSConfig;
43  import org.apache.ws.security.WSSecurityException;
44  import org.apache.ws.security.handler.RequestData;
45  import org.apache.ws.security.handler.WSHandlerConstants;
46  import org.apache.ws.security.util.WSSecurityUtil;
47  import org.opensaml.SAMLAssertion;
48  import org.opensaml.SAMLException;
49  import org.w3c.dom.Document;
50  import org.w3c.dom.Element;
51  import org.w3c.dom.Node;
52  
53  /*  Most of this code was taken from org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor
54      and modified for our purpose, additions should have comments starting with 'KS: '
55      
56      This WSS4JOutInterceptor along with the wss4j library, handled the sender-vouches with a signed 
57      SAML Assertion correctly. The information to create that sender-vouches Assertion comes from the 
58      saml.properties file.
59      We needed to create another signed SAML Assertion that would hold the authenticated user and any 
60      SAML attributes along with the original voucher SAML Assertion. In the end the message would have two
61      signed SAML assertions. This was not possible with WSS4JOutInterceptor. see 'KS :' comments
62      
63  */
64  public class SamlTokenCxfOutInterceptor extends AbstractWSS4JInterceptor {
65      private static final Logger LOG = LogUtils
66              .getL7dLogger(SamlTokenCxfOutInterceptor.class);
67  
68      private static final Logger TIME_LOG = LogUtils
69              .getL7dLogger(SamlTokenCxfOutInterceptor.class,
70                            null,
71                            SamlTokenCxfOutInterceptor.class.getName() + "-Time");
72      private SamlTokenCxfOutInterceptorInternal ending;
73      private SAAJOutInterceptor saajOut = new SAAJOutInterceptor();
74      private boolean mtomEnabled;
75      
76      // KS : added to hold information used to create authenticated user, SAML Assertion
77      // KS : made this a ThreadLocal since we don't know if interceptors are thread-safe.
78      //private Map<String,String> samlProperties = new HashMap<String,String>();
79      private ThreadLocal<Map<String,String>> samlPropertiesHolder = new ThreadLocal<Map<String,String>>();
80      private ThreadLocal<SAMLAssertion> samlAssertionHolder = new ThreadLocal<SAMLAssertion>();;
81      
82      public SamlTokenCxfOutInterceptor() {
83          super();
84          setPhase(Phase.PRE_PROTOCOL);
85          getAfter().add(SAAJOutInterceptor.class.getName());
86          
87          ending = createEndingInterceptor();
88      }
89      
90      public SamlTokenCxfOutInterceptor(Map<String, Object> props) {
91          this();
92          setProperties(props);
93      }
94      
95      public boolean isAllowMTOM() {
96          return mtomEnabled;
97      }
98      /**
99       * Enable or disable mtom with WS-Security.   By default MTOM is disabled as
100      * attachments would not get encrypted or be part of the signature.
101      * @param mtomEnabled
102      */
103     public void setAllowMTOM(boolean allowMTOM) {
104         this.mtomEnabled = allowMTOM;
105     }
106 
107     public void handleMessage(SoapMessage mc) throws Fault {
108         //must turn off mtom when using WS-Sec so binary is inlined so it can
109         //be properly signed/encrypted/etc...
110         if (!mtomEnabled) {
111             mc.put(org.apache.cxf.message.Message.MTOM_ENABLED, false);
112         }
113         
114         if (mc.getContent(SOAPMessage.class) == null) {
115             saajOut.handleMessage(mc);
116         }
117         
118         mc.getInterceptorChain().add(ending);
119     }    
120     public void handleFault(SoapMessage message) {
121         saajOut.handleFault(message);
122     } 
123     
124     public final SamlTokenCxfOutInterceptorInternal createEndingInterceptor() {
125         return new SamlTokenCxfOutInterceptorInternal();
126     }
127     
128     final class SamlTokenCxfOutInterceptorInternal 
129         implements PhaseInterceptor<SoapMessage> {
130         public SamlTokenCxfOutInterceptorInternal() {
131             super();
132         }
133         
134         public void handleMessage(SoapMessage mc) throws Fault {
135             // KS : added so we can create the authenticated user SAML Assertion before the message is handled.
136             String wsuId = handleMessageUserSAML(mc);
137             
138             boolean doDebug = LOG.isLoggable(Level.FINE);
139             boolean doTimeDebug = TIME_LOG.isLoggable(Level.FINE);
140             SoapVersion version = mc.getVersion();
141     
142             long t0 = 0;
143             long t1 = 0;
144             long t2 = 0;
145     
146             if (doTimeDebug) {
147                 t0 = System.currentTimeMillis();
148             }
149     
150             if (doDebug) {
151                 LOG.fine("SamlTokenCxfOutInterceptor: enter handleMessage()");
152             }
153             
154             // KS : this has been the problem and why we needed to code our own WSS4JOutInterceptor.
155             // WSS4JOutInterceptor needed to expose RequestData out of here so you can add the signature parts
156             // see code below, before doSenderAction();  Essentially anything signature parts, gets singed by wss4j.
157             RequestData reqData = new RequestData();
158             
159             reqData.setMsgContext(mc);
160             
161             /*
162              * The overall try, just to have a finally at the end to perform some
163              * housekeeping.
164              */
165             try {
166                 /*
167                  * Get the action first.
168                  */
169                 Vector actions = new Vector();
170                 String action = getString(WSHandlerConstants.ACTION, mc);
171                 if (action == null) {
172                     throw new SoapFault(new Message("NO_ACTION", LOG), version
173                             .getReceiver());
174                 }
175     
176                 int doAction = WSSecurityUtil.decodeAction(action, actions);
177                 if (doAction == WSConstants.NO_SECURITY) {
178                     return;
179                 }
180     
181                 /*
182                  * For every action we need a username, so get this now. The
183                  * username defined in the deployment descriptor takes precedence.
184                  */
185                 reqData.setUsername((String) getOption(WSHandlerConstants.USER));
186                 if (reqData.getUsername() == null
187                         || reqData.getUsername().equals("")) {
188                     String username = (String) getProperty(reqData.getMsgContext(),
189                             WSHandlerConstants.USER);
190                     if (username != null) {
191                         reqData.setUsername(username);
192                     }
193                 }
194     
195                 /*
196                  * Now we perform some set-up for UsernameToken and Signature
197                  * functions. No need to do it for encryption only. Check if
198                  * username is available and then get a passowrd.
199                  */
200                 if ((doAction & (WSConstants.SIGN | WSConstants.UT | WSConstants.UT_SIGN)) != 0
201                         && (reqData.getUsername() == null
202                         || reqData.getUsername().equals(""))) {
203                     /*
204                      * We need a username - if none throw an SoapFault. For
205                      * encryption there is a specific parameter to get a username.
206                      */
207                     throw new SoapFault(new Message("NO_USERNAME", LOG), version
208                             .getReceiver());
209                 }
210                 if (doDebug) {
211                     LOG.fine("Action: " + doAction);
212                     LOG.fine("Actor: " + reqData.getActor());
213                 }
214                 /*
215                  * Now get the SOAP part from the request message and convert it
216                  * into a Document. This forces CXF to serialize the SOAP request
217                  * into FORM_STRING. This string is converted into a document.
218                  * During the FORM_STRING serialization CXF performs multi-ref of
219                  * complex data types (if requested), generates and inserts
220                  * references for attachements and so on. The resulting Document
221                  * MUST be the complete and final SOAP request as CXF would send it
222                  * over the wire. Therefore this must shall be the last (or only)
223                  * handler in a chain. Now we can perform our security operations on
224                  * this request.
225                  */
226                 SOAPMessage saaj = mc.getContent(SOAPMessage.class);
227     
228                 if (saaj == null) {
229                     LOG.warning("SAAJOutHandler must be enabled for WS-Security!");
230                     throw new SoapFault(new Message("NO_SAAJ_DOC", LOG), version
231                             .getReceiver());
232                 }
233     
234                 Document doc = saaj.getSOAPPart();
235                 /**
236                  * There is nothing to send...Usually happens when the provider
237                  * needs to send a HTTP 202 message (with no content)
238                  */
239                 if (mc == null) {
240                     return;
241                 }
242     
243                 if (doTimeDebug) {
244                     t1 = System.currentTimeMillis();
245                 }
246                 
247                 // KS : any element represented by an encryptionPart and added to signatureParts will be signed.
248                 // The wsuId is the SAML Assertion we created above in handleMessageUserSAML(mc).
249                 WSEncryptionPart encP = new WSEncryptionPart(wsuId);
250                 reqData.getSignatureParts().add(encP);
251                 // KS : add the body, so it is also singed
252                 WSEncryptionPart encPBody = new WSEncryptionPart("Body", 
253                         doc.getDocumentElement().getNamespaceURI(), "Content");
254                 reqData.getSignatureParts().add(encPBody);
255                 
256                 
257                 doSenderAction(doAction, doc, reqData, actions, Boolean.TRUE
258                         .equals(getProperty(mc, org.apache.cxf.message.Message.REQUESTOR_ROLE)));
259                 
260                 if (doTimeDebug) {
261                     t2 = System.currentTimeMillis();
262                     TIME_LOG.fine("Send request: total= " + (t2 - t0)
263                             + " request preparation= " + (t1 - t0)
264                             + " request processing= " + (t2 - t1)
265                             + "\n");
266                 }
267     
268                 if (doDebug) {
269                     LOG.fine("SamlTokenCxfOutInterceptor: exit handleMessage()");
270                 }
271             } catch (WSSecurityException e) {
272                 throw new SoapFault(new Message("SECURITY_FAILED", LOG), e, version
273                         .getSender());
274             } finally {
275                 reqData.clear();
276                 reqData = null;
277             }
278         }
279         
280         public Set<String> getAfter() {
281             return Collections.emptySet();
282         }
283 
284         public Set<String> getBefore() {
285             return Collections.emptySet();
286         }
287 
288         public String getId() {
289             return SamlTokenCxfOutInterceptorInternal.class.getName();
290         }
291 
292         public String getPhase() {
293             return Phase.POST_PROTOCOL;
294         }
295 
296         @Override
297         public Collection<PhaseInterceptor<? extends org.apache.cxf.message.Message>> getAdditionalInterceptors() {
298             return null;  //To change body of implemented methods use File | Settings | File Templates.
299         }
300 
301         public void handleFault(SoapMessage message) {
302             //nothing
303         }
304         
305         // KS : All three methods below added to create the authenticated user SAML Assertion
306         public String handleMessageUserSAML(SoapMessage msg) throws Fault {
307             String wsuId = null;
308             Node assertionNode = null;
309             
310 /*            String user = getSamlProperties().get("user");
311             String pgt = getSamlProperties().get("proxyGrantingTicket");
312             String proxies = getSamlProperties().get("proxies");
313             String issuer = getSamlProperties().get("samlIssuerForUser");
314             String nameQualifier = getSamlProperties().get("samlIssuerForUser");
315             
316             SAMLAssertion assertion = new SAMLAssertion();
317             assertion.setIssuer(issuer);
318             
319             try{   
320                 // prepare subject
321                 SAMLNameIdentifier nameId = new SAMLNameIdentifier(user, nameQualifier, "");
322                 String[] confirmationMethods = {SAMLSubject.CONF_SENDER_VOUCHES};
323                 SAMLSubject subject = new SAMLSubject();
324                 subject.setNameIdentifier(nameId);
325                 subject.setConfirmationMethods(Arrays.asList(confirmationMethods));
326             
327                 // prepare auth statement
328                 SAMLAuthenticationStatement authStmt = new SAMLAuthenticationStatement();
329                 authStmt.setAuthInstant(new Date());
330                 authStmt.setAuthMethod(SAMLAuthenticationStatement.AuthenticationMethod_Password);
331                 authStmt.setSubject(subject);
332             
333                 // prepare attributes
334                 SAMLAttributeStatement attrStatement = new SAMLAttributeStatement();
335                 SAMLAttribute attr1 = new SAMLAttribute();
336                 attr1.setName("proxyGrantingTicket");
337                 attr1.setNamespace("Namesapce_of_Attribute1");
338             
339                 SAMLAttribute attr2 = new SAMLAttribute();
340                 attr2.setName("proxies");
341                 attr2.setNamespace("Namesapce_of_Attribute2");
342             
343                 
344                 attr1.addValue(pgt);
345                 attr1.addValue("additional value for proxy granting ticket");
346                 attr2.addValue(proxies);
347                 attr2.addValue("additional value for proxies");
348                 
349                 attrStatement.addAttribute(attr1);
350                 attrStatement.addAttribute(attr2);
351                 
352                 SAMLSubject subjectInAttr = (SAMLSubject)subject.clone();
353                 attrStatement.setSubject(subjectInAttr);
354                 
355                 // prepare Assertion
356                 assertion.addStatement(authStmt);
357                 assertion.addStatement(attrStatement);
358                 
359                 assertionNode = assertion.toDOM();
360                 
361             } catch(SAMLException se){
362                 Logger log = Logger.getLogger(SamlTokenCxfOutInterceptor.class.getName());
363                 throw new Fault("Error when adding SAML Attributes or Attribute Statement : ", log, se);
364             } catch(CloneNotSupportedException cnse){
365                 Logger log = Logger.getLogger(SamlTokenCxfOutInterceptor.class.getName());
366                 throw new Fault("Error when cloning subject : ", log, cnse);
367             } */
368             
369             try{
370                 assertionNode = getSamlAssertion().toDOM();
371             } catch(SAMLException se){
372                 Logger log = Logger.getLogger(SamlTokenCxfOutInterceptor.class.getName());
373                 throw new Fault("Error when adding SAML Attributes or Attribute Statement : ", log, se);
374             }
375             
376             try{
377                 // get the envelope and create a header
378                 SOAPMessage soapMsg = msg.getContent(SOAPMessage.class);
379                 SOAPPart doc = soapMsg.getSOAPPart();
380                 SOAPEnvelope envelope = doc.getEnvelope();
381                 envelope.addHeader();
382                 SOAPHeader soapHeader = soapMsg.getSOAPHeader();
383                 
384                 
385                 SOAPFactory soapFactory = SOAPFactory.newInstance();
386                 
387                 // create wsse:Security element
388                 SOAPElement wsseSecurity = soapFactory.createElement("Security", "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
389                 
390                 // insert assertion in wsse:Security element
391                 if(assertionNode.getNodeType() == Node.ELEMENT_NODE ){
392                     Element assertionElement = (Element)assertionNode;
393                     
394                     // add a wsu:Id
395                     wsuId = setWsuId(assertionElement);
396                     
397                     SOAPElement assertionSOAP = soapFactory.createElement(assertionElement);
398                     wsseSecurity.addChildElement(assertionSOAP);
399                 }
400                 // insert wsse:Security in header element
401                 soapHeader.addChildElement(wsseSecurity);
402             } catch(SOAPException se){
403                 throw new Fault(se);
404             }
405             return wsuId;
406         }
407         
408         protected String setWsuId(Element bodyElement) {
409             String id = bodyElement.getAttributeNS(WSConstants.WSU_NS, "Id");
410 
411             if ((id == null) || (id.length() == 0)) {
412                 id = WSSConfig.getNewInstance().getIdAllocator().createId("id-", bodyElement);
413                 String prefix = 
414                     WSSecurityUtil.setNamespace(bodyElement, WSConstants.WSU_NS, WSConstants.WSU_PREFIX);
415                 bodyElement.setAttributeNS(WSConstants.WSU_NS, prefix + ":Id", id);
416             }
417             return id;
418         }
419     }
420     
421     public void setSamlProperties(Map<String, String> samlProperties){
422         //this.samlProperties = samlProperties;
423         this.samlPropertiesHolder.set(samlProperties);
424     }
425 
426     public Map<String, String> getSamlProperties() {
427         //return samlProperties;
428         return this.samlPropertiesHolder.get();
429     }
430 
431     public SAMLAssertion getSamlAssertion() {
432         return this.samlAssertionHolder.get();
433     }
434 
435     public void setSamlAssertion(SAMLAssertion samlAssertion) {
436         this.samlAssertionHolder.set(samlAssertion);
437     }
438 }