001    /**
002     * Copyright 2010 The Kuali Foundation Licensed under the
003     * Educational Community License, Version 2.0 (the "License"); you may
004     * not use this file except in compliance with the License. You may
005     * obtain a copy of the License at
006     *
007     * http://www.osedu.org/licenses/ECL-2.0
008     *
009     * Unless required by applicable law or agreed to in writing,
010     * software distributed under the License is distributed on an "AS IS"
011     * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
012     * or implied. See the License for the specific language governing
013     * permissions and limitations under the License.
014     */
015    
016    package org.kuali.student.security.cxf.interceptors;
017    
018    import java.util.Collections;
019    import java.util.Map;
020    import java.util.Set;
021    import java.util.Vector;
022    import java.util.logging.Level;
023    import java.util.logging.Logger;
024    
025    import javax.xml.soap.SOAPElement;
026    import javax.xml.soap.SOAPEnvelope;
027    import javax.xml.soap.SOAPException;
028    import javax.xml.soap.SOAPFactory;
029    import javax.xml.soap.SOAPHeader;
030    import javax.xml.soap.SOAPMessage;
031    import javax.xml.soap.SOAPPart;
032    
033    import org.apache.cxf.binding.soap.SoapFault;
034    import org.apache.cxf.binding.soap.SoapMessage;
035    import org.apache.cxf.binding.soap.SoapVersion;
036    import org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor;
037    import org.apache.cxf.common.i18n.Message;
038    import org.apache.cxf.common.logging.LogUtils;
039    import org.apache.cxf.interceptor.Fault;
040    import org.apache.cxf.phase.Phase;
041    import org.apache.cxf.phase.PhaseInterceptor;
042    import org.apache.cxf.ws.security.wss4j.AbstractWSS4JInterceptor;
043    import org.apache.ws.security.WSConstants;
044    import org.apache.ws.security.WSEncryptionPart;
045    import org.apache.ws.security.WSSConfig;
046    import org.apache.ws.security.WSSecurityException;
047    import org.apache.ws.security.handler.RequestData;
048    import org.apache.ws.security.handler.WSHandlerConstants;
049    import org.apache.ws.security.util.WSSecurityUtil;
050    import org.opensaml.SAMLAssertion;
051    import org.opensaml.SAMLException;
052    import org.w3c.dom.Document;
053    import org.w3c.dom.Element;
054    import org.w3c.dom.Node;
055    
056    /*  Most of this code was taken from org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor
057        and modified for our purpose, additions should have comments starting with 'KS: '
058        
059        This WSS4JOutInterceptor along with the wss4j library, handled the sender-vouches with a signed 
060        SAML Assertion correctly. The information to create that sender-vouches Assertion comes from the 
061        saml.properties file.
062        We needed to create another signed SAML Assertion that would hold the authenticated user and any 
063        SAML attributes along with the original voucher SAML Assertion. In the end the message would have two
064        signed SAML assertions. This was not possible with WSS4JOutInterceptor. see 'KS :' comments
065        
066    */
067    public class SamlTokenCxfOutInterceptor extends AbstractWSS4JInterceptor {
068        private static final Logger LOG = LogUtils
069                .getL7dLogger(SamlTokenCxfOutInterceptor.class);
070    
071        private static final Logger TIME_LOG = LogUtils
072                .getL7dLogger(SamlTokenCxfOutInterceptor.class,
073                              null,
074                              SamlTokenCxfOutInterceptor.class.getName() + "-Time");
075        private SamlTokenCxfOutInterceptorInternal ending;
076        private SAAJOutInterceptor saajOut = new SAAJOutInterceptor();
077        private boolean mtomEnabled;
078        
079        // KS : added to hold information used to create authenticated user, SAML Assertion
080        // KS : made this a ThreadLocal since we don't know if interceptors are thread-safe.
081        //private Map<String,String> samlProperties = new HashMap<String,String>();
082        private ThreadLocal<Map<String,String>> samlPropertiesHolder = new ThreadLocal<Map<String,String>>();
083        private ThreadLocal<SAMLAssertion> samlAssertionHolder = new ThreadLocal<SAMLAssertion>();;
084        
085        public SamlTokenCxfOutInterceptor() {
086            super();
087            setPhase(Phase.PRE_PROTOCOL);
088            getAfter().add(SAAJOutInterceptor.class.getName());
089            
090            ending = createEndingInterceptor();
091        }
092        
093        public SamlTokenCxfOutInterceptor(Map<String, Object> props) {
094            this();
095            setProperties(props);
096        }
097        
098        public boolean isAllowMTOM() {
099            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    }