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 }