1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package edu.internet2.middleware.shibboleth.idp.profile.saml2;
18
19 import java.io.OutputStreamWriter;
20 import java.io.Writer;
21 import java.util.ArrayList;
22
23 import javax.servlet.http.HttpServletRequest;
24
25 import org.joda.time.DateTime;
26 import org.opensaml.Configuration;
27 import org.opensaml.common.SAMLObjectBuilder;
28 import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
29 import org.opensaml.common.xml.SAMLConstants;
30 import org.opensaml.saml2.binding.decoding.HandlerChainAwareHTTPSOAP11Decoder;
31 import org.opensaml.saml2.binding.encoding.HandlerChainAwareHTTPSOAP11Encoder;
32 import org.opensaml.saml2.core.AttributeStatement;
33 import org.opensaml.saml2.core.AuthnContext;
34 import org.opensaml.saml2.core.AuthnContextClassRef;
35 import org.opensaml.saml2.core.AuthnRequest;
36 import org.opensaml.saml2.core.AuthnStatement;
37 import org.opensaml.saml2.core.Response;
38 import org.opensaml.saml2.core.Statement;
39 import org.opensaml.saml2.core.StatusCode;
40 import org.opensaml.saml2.core.Subject;
41 import org.opensaml.saml2.core.SubjectConfirmation;
42 import org.opensaml.saml2.metadata.SPSSODescriptor;
43 import org.opensaml.ws.message.decoder.MessageDecodingException;
44
45 import org.opensaml.ws.transport.http.HTTPInTransport;
46 import org.opensaml.ws.transport.http.HTTPOutTransport;
47 import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
48 import org.opensaml.xml.security.SecurityException;
49 import org.opensaml.xml.util.DatatypeHelper;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
54 import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
55 import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfiguration;
56 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
57 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.ECPConfiguration;
58
59 import org.opensaml.ws.message.handler.BasicHandlerChain;
60 import org.opensaml.ws.message.handler.Handler;
61 import org.opensaml.ws.message.handler.HandlerChain;
62 import org.opensaml.ws.message.handler.HandlerChainResolver;
63 import org.opensaml.ws.message.handler.HandlerException;
64 import org.opensaml.ws.message.handler.StaticHandlerChainResolver;
65 import org.opensaml.ws.message.MessageContext;
66 import org.opensaml.ws.soap.soap11.ActorBearing;
67 import org.opensaml.ws.soap.util.SOAPHelper;
68
69 import org.opensaml.common.binding.SAMLMessageContext;
70 import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
71
72
73 public class SAML2ECPProfileHandler extends SSOProfileHandler {
74
75
76 private final Logger log = LoggerFactory.getLogger(SAML2ECPProfileHandler.class);
77
78
79 private String authnContextClassRef = AuthnContext.PPT_AUTHN_CTX;
80
81
82 private SAMLObjectBuilder<org.opensaml.saml2.ecp.Response> ecpResponseBuilder;
83
84
85 private SAMLObjectBuilder<AuthnContext> authnContextBuilder;
86
87
88 private SAMLObjectBuilder<AuthnContextClassRef> authnContextClassRefBuilder;
89
90
91 private StaticHandlerChainResolver inboundPreSecurityHandlerChainResolver;
92
93
94 private StaticHandlerChainResolver inboundPostSecurityHandlerChainResolver;
95
96
97 private StaticHandlerChainResolver outboundHandlerChainResolver;
98
99
100 private SAMLMessageEncoder messageEncoder;
101
102
103 private SAMLMessageDecoder messageDecoder;
104
105
106 private static String soapFaultResponseMessage =
107 "<env:Envelope xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
108 " <env:Body>" +
109 " <env:Fault>" +
110 " <faultcode>env:Client</faultcode>" +
111 " <faultstring>An error occurred processing the request.</faultstring>" +
112 " <detail/>" +
113 " </env:Fault>" +
114 " </env:Body>" +
115 "</env:Envelope>";
116
117
118
119
120
121
122 @SuppressWarnings("unchecked")
123 public SAML2ECPProfileHandler() {
124 super("/Save/My/Walrus");
125
126 ecpResponseBuilder = (SAMLObjectBuilder<org.opensaml.saml2.ecp.Response>) Configuration.getBuilderFactory().
127 getBuilder(org.opensaml.saml2.ecp.Response.DEFAULT_ELEMENT_NAME);
128
129 authnContextBuilder = (SAMLObjectBuilder<AuthnContext>) getBuilderFactory().getBuilder(
130 AuthnContext.DEFAULT_ELEMENT_NAME);
131 authnContextClassRefBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) getBuilderFactory().getBuilder(
132 AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
133 }
134
135
136 public void initialize() {
137 messageDecoder = new HandlerChainAwareHTTPSOAP11Decoder();
138 messageEncoder = new HandlerChainAwareHTTPSOAP11Encoder();
139 ((HandlerChainAwareHTTPSOAP11Encoder) messageEncoder).setNotConfidential(true);
140
141 inboundPreSecurityHandlerChainResolver = new StaticHandlerChainResolver(buildPreSecurityInboundHandlerChain());
142 inboundPostSecurityHandlerChainResolver = new StaticHandlerChainResolver(buildPostSecurityInboundHandlerChain());
143 outboundHandlerChainResolver = new StaticHandlerChainResolver(buildOutboundHandlerChain());
144
145
146
147
148 ArrayList<String> ecpOutboundBindings = new ArrayList<String>();
149 ecpOutboundBindings.add(SAMLConstants.SAML2_PAOS_BINDING_URI);
150 setSupportedOutboundBindings(ecpOutboundBindings);
151 }
152
153
154
155 public String getProfileId() {
156 return ECPConfiguration.PROFILE_ID;
157 }
158
159
160
161
162
163 public void setAuthnContextClassRef(String ref) {
164 authnContextClassRef = ref;
165 }
166
167
168
169
170
171 public String getAuthnContextClassRef() {
172 return authnContextClassRef;
173 }
174
175
176 public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
177 ECPRequestContext requestContext = buildRequestContext(inTransport, outTransport);
178
179 Response samlResponse;
180
181 try {
182 decodeRequest(requestContext, inTransport, outTransport);
183 checkSamlVersion(requestContext);
184 checkNameIDPolicy(requestContext);
185
186 if (requestContext.getPrincipalName() == null) {
187 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI,
188 null));
189 throw new ProfileException("Authentication not performed");
190 }
191
192 if (requestContext.getSubjectNameIdentifier() != null) {
193 log.debug("Authentication request contained a subject with a name identifier, resolving principal from NameID");
194 String authenticatedName = requestContext.getPrincipalName();
195 resolvePrincipal(requestContext);
196 String requestedPrincipalName = requestContext.getPrincipalName();
197 if (!DatatypeHelper.safeEquals(authenticatedName, requestedPrincipalName)) {
198 log.warn(
199 "Authentication request identified principal {} but authentication mechanism identified principal {}",
200 requestedPrincipalName, authenticatedName);
201 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI,
202 null));
203 throw new ProfileException("User failed authentication");
204 }
205 }
206
207 String relyingPartyId = requestContext.getInboundMessageIssuer();
208 RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
209 ProfileConfiguration ecpConfig = rpConfig.getProfileConfiguration(getProfileId());
210 if (ecpConfig == null) {
211 log.warn("SAML2ECP profile is not configured for relying party '{}'", requestContext.getInboundMessageIssuer());
212 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_UNSUPPORTED_URI,
213 null));
214 throw new ProfileException("SAML2ECP profile is not configured for relying party");
215 }
216
217 resolveAttributes(requestContext);
218
219 ArrayList<Statement> statements = new ArrayList<Statement>();
220 statements.add(buildAuthnStatement(requestContext));
221 if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
222 AttributeStatement attributeStatement = buildAttributeStatement(requestContext);
223 if (attributeStatement != null) {
224 requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
225 statements.add(attributeStatement);
226 }
227 }
228
229 samlResponse = buildResponse(requestContext, SubjectConfirmation.METHOD_BEARER, statements);
230 samlResponse.setDestination(requestContext.getPeerEntityEndpoint().getLocation());
231
232 } catch (ProfileException e) {
233 if (requestContext.getPeerEntityEndpoint() != null) {
234 samlResponse = buildErrorResponse(requestContext);
235 } else {
236 log.debug("Returning SOAP fault", e);
237 try {
238 outTransport.setCharacterEncoding("UTF-8");
239 outTransport.setHeader("Content-Type", "application/soap+xml");
240 outTransport.setStatusCode(500);
241 Writer out = new OutputStreamWriter(outTransport.getOutgoingStream(), "UTF-8");
242 out.write(soapFaultResponseMessage);
243 out.flush();
244 } catch (Exception we) {
245 log.error("Error returning SOAP fault", we);
246 }
247 return;
248 }
249 }
250
251 requestContext.setOutboundSAMLMessage(samlResponse);
252 requestContext.setOutboundSAMLMessageId(samlResponse.getID());
253 requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
254 encodeResponse(requestContext);
255 writeAuditLogEntry(requestContext);
256 }
257
258
259
260
261
262
263
264
265
266
267 protected void decodeRequest(ECPRequestContext requestContext, HTTPInTransport inTransport,
268 HTTPOutTransport outTransport) throws ProfileException {
269 if (log.isDebugEnabled()) {
270 log.debug("Decoding message with decoder binding '{}'", getInboundMessageDecoder(requestContext)
271 .getBindingURI());
272 }
273
274 try {
275 SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
276 requestContext.setMessageDecoder(decoder);
277 decoder.decode(requestContext);
278 log.debug("Decoded request from relying party '{}'", requestContext.getInboundMessageIssuer());
279
280 if (!(requestContext.getInboundSAMLMessage() instanceof AuthnRequest)) {
281 log.warn("Incomming message was not a AuthnRequest, it was a '{}'", requestContext
282 .getInboundSAMLMessage().getClass().getName());
283 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_UNSUPPORTED_URI,
284 "Invalid SAML AuthnRequest message."));
285 throw new ProfileException("Invalid SAML AuthnRequest message.");
286 }
287
288 AuthnRequest authnRequest = requestContext.getInboundSAMLMessage();
289 Subject authnSubject = authnRequest.getSubject();
290 if (authnSubject != null) {
291 requestContext.setSubjectNameIdentifier(authnSubject.getNameID());
292 }
293 } catch (MessageDecodingException e) {
294 String msg = "Error decoding authentication request message";
295 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_UNSUPPORTED_URI, msg));
296 log.warn(msg, e);
297 throw new ProfileException(msg, e);
298 } catch (SecurityException e) {
299 String msg = "Message did not meet security requirements";
300 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_DENIED_URI, msg));
301 log.warn(msg, e);
302 throw new ProfileException(msg, e);
303 }
304 populateRequestContext(requestContext);
305 }
306
307
308
309
310
311
312
313
314
315
316
317 protected ECPRequestContext buildRequestContext(HTTPInTransport in, HTTPOutTransport out)
318 throws ProfileException {
319 ECPRequestContext requestContext = new ECPRequestContext();
320
321 requestContext.setCommunicationProfileId(getProfileId());
322 requestContext.setMessageDecoder(getInboundMessageDecoder(requestContext));
323 requestContext.setInboundMessageTransport(in);
324 requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
325 requestContext.setOutboundMessageTransport(out);
326 requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
327 requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
328 requestContext.setMetadataProvider(getMetadataProvider());
329 requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
330
331
332 String relyingPartyId = requestContext.getInboundMessageIssuer();
333 requestContext.setPeerEntityId(relyingPartyId);
334 requestContext.setInboundMessageIssuer(relyingPartyId);
335
336 requestContext.setPreSecurityInboundHandlerChainResolver(getPreSecurityInboundHandlerChainResolver());
337 requestContext.setPostSecurityInboundHandlerChainResolver(getPostSecurityInboundHandlerChainResolver());
338 requestContext.setOutboundHandlerChainResolver(getOutboundHandlerChainResolver());
339
340 return requestContext;
341 }
342
343
344 protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
345 AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
346 Subject authnSubject = authnRequest.getSubject();
347 if (authnSubject != null) {
348 requestContext.setSubjectNameIdentifier(authnSubject.getNameID());
349 }
350 }
351
352
353
354
355
356
357
358
359 protected AuthnStatement buildAuthnStatement(SSORequestContext requestContext) {
360 AuthnStatement statement = super.buildAuthnStatement(requestContext);
361 statement.setAuthnInstant(new DateTime());
362 return statement;
363 }
364
365
366
367
368
369
370
371
372 protected AuthnContext buildAuthnContext(SSORequestContext requestContext) {
373 if (getAuthnContextClassRef() != null) {
374 AuthnContext authnContext = authnContextBuilder.buildObject();
375 AuthnContextClassRef ref = authnContextClassRefBuilder.buildObject();
376 ref.setAuthnContextClassRef(getAuthnContextClassRef());
377 authnContext.setAuthnContextClassRef(ref);
378 return authnContext;
379 }
380 return null;
381 }
382
383
384
385 protected class ECPRequestContext extends SSORequestContext {
386 }
387
388
389
390
391
392
393
394 protected HandlerChain buildPreSecurityInboundHandlerChain() {
395 BasicHandlerChain handlerChain = new BasicHandlerChain();
396
397 handlerChain.getHandlers().add( new Handler() {
398 public void invoke(MessageContext msgContext) throws HandlerException {
399 ECPRequestContext ctx = (ECPRequestContext) msgContext;
400 HttpServletRequest httpRequest =
401 ((HttpServletRequestAdapter) msgContext.getInboundMessageTransport()).getWrappedRequest();
402 String user = httpRequest.getRemoteUser();
403 if (user != null) {
404 log.debug("Setting principal name: {}", user);
405 ctx.setPrincipalName(user);
406 } else {
407 log.warn("REMOTE_USER not set, unable to set principal name");
408 }
409 }
410 });
411
412 return handlerChain;
413 }
414
415
416
417
418
419
420 protected HandlerChain buildPostSecurityInboundHandlerChain() {
421 return null;
422 }
423
424
425
426
427
428
429 protected HandlerChainResolver getPreSecurityInboundHandlerChainResolver() {
430 return inboundPreSecurityHandlerChainResolver;
431 }
432
433
434
435
436
437
438 protected HandlerChainResolver getPostSecurityInboundHandlerChainResolver() {
439 return inboundPostSecurityHandlerChainResolver;
440 }
441
442
443
444
445
446
447 protected HandlerChain buildOutboundHandlerChain() {
448 BasicHandlerChain handlerChain = new BasicHandlerChain();
449
450 handlerChain.getHandlers().add( new Handler() {
451 public void invoke(MessageContext msgContext) throws HandlerException {
452 SAMLMessageContext samlMsgCtx = (SAMLMessageContext) msgContext;
453 org.opensaml.saml2.ecp.Response response = ecpResponseBuilder.buildObject();
454 if (samlMsgCtx.getPeerEntityEndpoint() == null || samlMsgCtx.getPeerEntityEndpoint().getLocation() == null) {
455 throw new HandlerException("Unable to determine ACS URL for response.");
456 }
457 response.setAssertionConsumerServiceURL(samlMsgCtx.getPeerEntityEndpoint().getLocation());
458 SOAPHelper.addSOAP11MustUnderstandAttribute(response, true);
459 SOAPHelper.addSOAP11ActorAttribute(response, ActorBearing.SOAP11_ACTOR_NEXT);
460 SOAPHelper.addHeaderBlock(msgContext, response);
461 }
462 });
463
464 return handlerChain;
465 }
466
467
468
469
470
471
472 protected HandlerChainResolver getOutboundHandlerChainResolver() {
473 return outboundHandlerChainResolver;
474 }
475
476
477 protected SAMLMessageEncoder getOutboundMessageEncoder(BaseSAMLProfileRequestContext requestContext)
478 throws ProfileException {
479 return messageEncoder;
480 }
481
482
483 protected SAMLMessageDecoder getInboundMessageDecoder(BaseSAMLProfileRequestContext requestContext)
484 throws ProfileException {
485 return messageDecoder;
486 }
487 }