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