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