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 }