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