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 | } |