| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
| SamlTokenCxfOutInterceptor |
|
| 2.6;2.6 | ||||
| SamlTokenCxfOutInterceptor$SamlTokenCxfOutInterceptorInternal |
|
| 2.6;2.6 |
| 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 | 0 | public class SamlTokenCxfOutInterceptor extends AbstractWSS4JInterceptor { |
| 68 | 0 | private static final Logger LOG = LogUtils |
| 69 | .getL7dLogger(SamlTokenCxfOutInterceptor.class); | |
| 70 | ||
| 71 | 0 | private static final Logger TIME_LOG = LogUtils |
| 72 | .getL7dLogger(SamlTokenCxfOutInterceptor.class, | |
| 73 | null, | |
| 74 | SamlTokenCxfOutInterceptor.class.getName() + "-Time"); | |
| 75 | private SamlTokenCxfOutInterceptorInternal ending; | |
| 76 | 0 | 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 | 0 | private ThreadLocal<Map<String,String>> samlPropertiesHolder = new ThreadLocal<Map<String,String>>(); |
| 83 | 0 | private ThreadLocal<SAMLAssertion> samlAssertionHolder = new ThreadLocal<SAMLAssertion>();; |
| 84 | ||
| 85 | public SamlTokenCxfOutInterceptor() { | |
| 86 | 0 | super(); |
| 87 | 0 | setPhase(Phase.PRE_PROTOCOL); |
| 88 | 0 | getAfter().add(SAAJOutInterceptor.class.getName()); |
| 89 | ||
| 90 | 0 | ending = createEndingInterceptor(); |
| 91 | 0 | } |
| 92 | ||
| 93 | public SamlTokenCxfOutInterceptor(Map<String, Object> props) { | |
| 94 | 0 | this(); |
| 95 | 0 | setProperties(props); |
| 96 | 0 | } |
| 97 | ||
| 98 | public boolean isAllowMTOM() { | |
| 99 | 0 | 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 | 0 | this.mtomEnabled = allowMTOM; |
| 108 | 0 | } |
| 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 | 0 | if (!mtomEnabled) { |
| 114 | 0 | mc.put(org.apache.cxf.message.Message.MTOM_ENABLED, false); |
| 115 | } | |
| 116 | ||
| 117 | 0 | if (mc.getContent(SOAPMessage.class) == null) { |
| 118 | 0 | saajOut.handleMessage(mc); |
| 119 | } | |
| 120 | ||
| 121 | 0 | mc.getInterceptorChain().add(ending); |
| 122 | 0 | } |
| 123 | public void handleFault(SoapMessage message) { | |
| 124 | 0 | saajOut.handleFault(message); |
| 125 | 0 | } |
| 126 | ||
| 127 | public final SamlTokenCxfOutInterceptorInternal createEndingInterceptor() { | |
| 128 | 0 | return new SamlTokenCxfOutInterceptorInternal(); |
| 129 | } | |
| 130 | ||
| 131 | 0 | final class SamlTokenCxfOutInterceptorInternal |
| 132 | implements PhaseInterceptor<SoapMessage> { | |
| 133 | 0 | public SamlTokenCxfOutInterceptorInternal() { |
| 134 | 0 | super(); |
| 135 | 0 | } |
| 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 | 0 | String wsuId = handleMessageUserSAML(mc); |
| 140 | ||
| 141 | 0 | boolean doDebug = LOG.isLoggable(Level.FINE); |
| 142 | 0 | boolean doTimeDebug = TIME_LOG.isLoggable(Level.FINE); |
| 143 | 0 | SoapVersion version = mc.getVersion(); |
| 144 | ||
| 145 | 0 | long t0 = 0; |
| 146 | 0 | long t1 = 0; |
| 147 | 0 | long t2 = 0; |
| 148 | ||
| 149 | 0 | if (doTimeDebug) { |
| 150 | 0 | t0 = System.currentTimeMillis(); |
| 151 | } | |
| 152 | ||
| 153 | 0 | if (doDebug) { |
| 154 | 0 | 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 | 0 | RequestData reqData = new RequestData(); |
| 161 | ||
| 162 | 0 | 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 | 0 | Vector actions = new Vector(); |
| 173 | 0 | String action = getString(WSHandlerConstants.ACTION, mc); |
| 174 | 0 | if (action == null) { |
| 175 | 0 | throw new SoapFault(new Message("NO_ACTION", LOG), version |
| 176 | .getReceiver()); | |
| 177 | } | |
| 178 | ||
| 179 | 0 | int doAction = WSSecurityUtil.decodeAction(action, actions); |
| 180 | 0 | 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 | 0 | reqData.setUsername((String) getOption(WSHandlerConstants.USER)); |
| 189 | 0 | if (reqData.getUsername() == null |
| 190 | || reqData.getUsername().equals("")) { | |
| 191 | 0 | String username = (String) getProperty(reqData.getMsgContext(), |
| 192 | WSHandlerConstants.USER); | |
| 193 | 0 | if (username != null) { |
| 194 | 0 | 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 | 0 | 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 | 0 | throw new SoapFault(new Message("NO_USERNAME", LOG), version |
| 211 | .getReceiver()); | |
| 212 | } | |
| 213 | 0 | if (doDebug) { |
| 214 | 0 | LOG.fine("Action: " + doAction); |
| 215 | 0 | 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 | 0 | SOAPMessage saaj = mc.getContent(SOAPMessage.class); |
| 230 | ||
| 231 | 0 | if (saaj == null) { |
| 232 | 0 | LOG.warning("SAAJOutHandler must be enabled for WS-Security!"); |
| 233 | 0 | throw new SoapFault(new Message("NO_SAAJ_DOC", LOG), version |
| 234 | .getReceiver()); | |
| 235 | } | |
| 236 | ||
| 237 | 0 | 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 | 0 | if (mc == null) { |
| 243 | return; | |
| 244 | } | |
| 245 | ||
| 246 | 0 | if (doTimeDebug) { |
| 247 | 0 | 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 | 0 | WSEncryptionPart encP = new WSEncryptionPart(wsuId); |
| 253 | 0 | reqData.getSignatureParts().add(encP); |
| 254 | // KS : add the body, so it is also singed | |
| 255 | 0 | WSEncryptionPart encPBody = new WSEncryptionPart("Body", |
| 256 | doc.getDocumentElement().getNamespaceURI(), "Content"); | |
| 257 | 0 | reqData.getSignatureParts().add(encPBody); |
| 258 | ||
| 259 | ||
| 260 | 0 | doSenderAction(doAction, doc, reqData, actions, Boolean.TRUE |
| 261 | .equals(getProperty(mc, org.apache.cxf.message.Message.REQUESTOR_ROLE))); | |
| 262 | ||
| 263 | 0 | if (doTimeDebug) { |
| 264 | 0 | t2 = System.currentTimeMillis(); |
| 265 | 0 | TIME_LOG.fine("Send request: total= " + (t2 - t0) |
| 266 | + " request preparation= " + (t1 - t0) | |
| 267 | + " request processing= " + (t2 - t1) | |
| 268 | + "\n"); | |
| 269 | } | |
| 270 | ||
| 271 | 0 | if (doDebug) { |
| 272 | 0 | LOG.fine("SamlTokenCxfOutInterceptor: exit handleMessage()"); |
| 273 | } | |
| 274 | 0 | } catch (WSSecurityException e) { |
| 275 | 0 | throw new SoapFault(new Message("SECURITY_FAILED", LOG), e, version |
| 276 | .getSender()); | |
| 277 | } finally { | |
| 278 | 0 | reqData.clear(); |
| 279 | 0 | reqData = null; |
| 280 | 0 | } |
| 281 | 0 | } |
| 282 | ||
| 283 | public Set<String> getAfter() { | |
| 284 | 0 | return Collections.emptySet(); |
| 285 | } | |
| 286 | ||
| 287 | public Set<String> getBefore() { | |
| 288 | 0 | return Collections.emptySet(); |
| 289 | } | |
| 290 | ||
| 291 | public String getId() { | |
| 292 | 0 | return SamlTokenCxfOutInterceptorInternal.class.getName(); |
| 293 | } | |
| 294 | ||
| 295 | public String getPhase() { | |
| 296 | 0 | return Phase.POST_PROTOCOL; |
| 297 | } | |
| 298 | ||
| 299 | public void handleFault(SoapMessage message) { | |
| 300 | //nothing | |
| 301 | 0 | } |
| 302 | ||
| 303 | // KS : All three methods below added to create the authenticated user SAML Assertion | |
| 304 | public String handleMessageUserSAML(SoapMessage msg) throws Fault { | |
| 305 | 0 | String wsuId = null; |
| 306 | 0 | 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 | 0 | assertionNode = getSamlAssertion().toDOM(); |
| 369 | 0 | } catch(SAMLException se){ |
| 370 | 0 | Logger log = Logger.getLogger(SamlTokenCxfOutInterceptor.class.getName()); |
| 371 | 0 | throw new Fault("Error when adding SAML Attributes or Attribute Statement : ", log, se); |
| 372 | 0 | } |
| 373 | ||
| 374 | try{ | |
| 375 | // get the envelope and create a header | |
| 376 | 0 | SOAPMessage soapMsg = msg.getContent(SOAPMessage.class); |
| 377 | 0 | SOAPPart doc = soapMsg.getSOAPPart(); |
| 378 | 0 | SOAPEnvelope envelope = doc.getEnvelope(); |
| 379 | 0 | envelope.addHeader(); |
| 380 | 0 | SOAPHeader soapHeader = soapMsg.getSOAPHeader(); |
| 381 | ||
| 382 | ||
| 383 | 0 | SOAPFactory soapFactory = SOAPFactory.newInstance(); |
| 384 | ||
| 385 | // create wsse:Security element | |
| 386 | 0 | 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 | 0 | if(assertionNode.getNodeType() == Node.ELEMENT_NODE ){ |
| 390 | 0 | Element assertionElement = (Element)assertionNode; |
| 391 | ||
| 392 | // add a wsu:Id | |
| 393 | 0 | wsuId = setWsuId(assertionElement); |
| 394 | ||
| 395 | 0 | SOAPElement assertionSOAP = soapFactory.createElement(assertionElement); |
| 396 | 0 | wsseSecurity.addChildElement(assertionSOAP); |
| 397 | } | |
| 398 | // insert wsse:Security in header element | |
| 399 | 0 | soapHeader.addChildElement(wsseSecurity); |
| 400 | 0 | } catch(SOAPException se){ |
| 401 | 0 | throw new Fault(se); |
| 402 | 0 | } |
| 403 | 0 | return wsuId; |
| 404 | } | |
| 405 | ||
| 406 | protected String setWsuId(Element bodyElement) { | |
| 407 | 0 | String id = bodyElement.getAttributeNS(WSConstants.WSU_NS, "Id"); |
| 408 | ||
| 409 | 0 | if ((id == null) || (id.length() == 0)) { |
| 410 | 0 | id = WSSConfig.getDefaultWSConfig().getIdAllocator().createId("id-", bodyElement); |
| 411 | 0 | String prefix = |
| 412 | WSSecurityUtil.setNamespace(bodyElement, WSConstants.WSU_NS, WSConstants.WSU_PREFIX); | |
| 413 | 0 | bodyElement.setAttributeNS(WSConstants.WSU_NS, prefix + ":Id", id); |
| 414 | } | |
| 415 | 0 | return id; |
| 416 | } | |
| 417 | } | |
| 418 | ||
| 419 | public void setSamlProperties(Map<String, String> samlProperties){ | |
| 420 | //this.samlProperties = samlProperties; | |
| 421 | 0 | this.samlPropertiesHolder.set(samlProperties); |
| 422 | 0 | } |
| 423 | ||
| 424 | public Map<String, String> getSamlProperties() { | |
| 425 | //return samlProperties; | |
| 426 | 0 | return this.samlPropertiesHolder.get(); |
| 427 | } | |
| 428 | ||
| 429 | public SAMLAssertion getSamlAssertion() { | |
| 430 | 0 | return this.samlAssertionHolder.get(); |
| 431 | } | |
| 432 | ||
| 433 | public void setSamlAssertion(SAMLAssertion samlAssertion) { | |
| 434 | 0 | this.samlAssertionHolder.set(samlAssertion); |
| 435 | 0 | } |
| 436 | } |