View Javadoc

1   /*
2    * Copyright 2010 University Corporation for Advanced Internet Development, Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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  /** SAML 2.0 ECP request profile handler. */
73  public class SAML2ECPProfileHandler extends SSOProfileHandler {
74  
75      /** Class logger. */
76      private final Logger log = LoggerFactory.getLogger(SAML2ECPProfileHandler.class);
77  
78      /** A context class reference to insert into the assertion. */
79      private String authnContextClassRef = AuthnContext.PPT_AUTHN_CTX;
80      
81      /** Builder of ECP Response object. */
82      private SAMLObjectBuilder<org.opensaml.saml2.ecp.Response> ecpResponseBuilder;
83  
84      /** Builder of AuthnContext objects. */
85      private SAMLObjectBuilder<AuthnContext> authnContextBuilder;
86  
87      /** Builder of AuthnContextClassRef objects. */
88      private SAMLObjectBuilder<AuthnContextClassRef> authnContextClassRefBuilder;
89  
90      /** Static pre-security inbound handler chain resolver. */
91      private StaticHandlerChainResolver inboundPreSecurityHandlerChainResolver;
92  
93      /** Static post-security inbound handler chain resolver. */
94      private StaticHandlerChainResolver inboundPostSecurityHandlerChainResolver;
95      
96      /** Static outbound handler chain resolver. */
97      private StaticHandlerChainResolver outboundHandlerChainResolver;
98      
99      /** SOAP message encoder to use. */
100     private SAMLMessageEncoder messageEncoder;
101     
102     /** SOAP message decoder to use. */
103     private SAMLMessageDecoder messageDecoder;
104 
105     // canned soap fauilt
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      * Constructor.
120      * 
121      */
122     @SuppressWarnings("unchecked")
123     public SAML2ECPProfileHandler() {
124         super("/Save/My/Walrus");   // need a dummy value to build base class
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     /** Initialize the profile handler. */
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         // This is needed by the endpoint selector. We're obviously not actually responding over this binding.
146         // In this profile handler, outbound binding selection is not determined by the product of the 
147         // EndpointSelector.
148         ArrayList<String> ecpOutboundBindings = new ArrayList<String>();
149         ecpOutboundBindings.add(SAMLConstants.SAML2_PAOS_BINDING_URI);
150         setSupportedOutboundBindings(ecpOutboundBindings);
151     }
152 
153 
154     /** {@inheritDoc} */
155     public String getProfileId() {
156         return ECPConfiguration.PROFILE_ID;
157     }
158 
159     /**
160      * Sets the AuthnContext class reference.
161      * @param ref AuthnContext class reference to set
162      */
163     public void setAuthnContextClassRef(String ref) {
164         authnContextClassRef = ref;
165     }
166 
167     /**
168      * Gets the AuthnContext class reference.
169      * @return AuthnContext class reference
170      */
171     public String getAuthnContextClassRef() {
172         return authnContextClassRef;
173     }
174 
175     /** {@inheritDoc} */
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);  // seem to lose the message when we report an error.
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      * Decodes an incoming request and stores the information in a created request context.
260      * 
261      * @param inTransport inbound transport
262      * @param outTransport outbound transport
263      * @param requestContext request context to which decoded information should be added
264      * 
265      * @throws ProfileException thrown if the incoming message failed decoding
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      * Creates an authentication request context from the current environmental information.
309      * 
310      * @param in inbound transport
311      * @param out outbount transport
312      * 
313      * @return created authentication request context
314      * 
315      * @throws ProfileException thrown if there is a problem creating the context
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         // Does this do anything?
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     /** {@inheritDoc} */
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      * Creates an authentication statement for the current request.
354      * 
355      * @param requestContext current request context
356      * 
357      * @return constructed authentication statement
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      * Creates an {@link AuthnContext} for a successful authentication request.
367      * 
368      * @param requestContext current request
369      * 
370      * @return the built authn context
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     /** Stub in case we define additional context data. */
384     /** In case we ever add something to the base context **/
385     protected class ECPRequestContext extends SSORequestContext {
386     }
387     
388 
389     /**
390      * Build the pre-security inbound handler chain.
391      *
392      * @return the handler chain
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      * Build the post-security inbound handler chain.
417      *
418      * @return the handler chain
419      */
420     protected HandlerChain buildPostSecurityInboundHandlerChain() {
421        return null;
422     }
423     
424     /**
425      * Get the resolver used to resolve the pre-security inbound handler chain.
426      *
427      * @return the handler chain resolver
428      */
429     protected HandlerChainResolver getPreSecurityInboundHandlerChainResolver() {
430         return inboundPreSecurityHandlerChainResolver;
431     }
432 
433     /**
434      * Get the resolver used to resolve the post-security inbound handler chain.
435      *
436      * @return the handler chain resolver
437      */
438     protected HandlerChainResolver getPostSecurityInboundHandlerChainResolver() {
439         return inboundPostSecurityHandlerChainResolver;
440     }
441     
442     /**
443      * Build the outbound handler chain.
444      *
445      * @return the handler chain
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      * Get the resolver used to resolve the outbound handler chain.
469      *
470      * @return the handler chain resolver
471      */
472     protected HandlerChainResolver getOutboundHandlerChainResolver() {
473         return outboundHandlerChainResolver;
474     }
475     
476     /** {@inheritDoc} */
477     protected SAMLMessageEncoder getOutboundMessageEncoder(BaseSAMLProfileRequestContext requestContext)
478             throws ProfileException {
479         return messageEncoder;
480     }
481 
482     /** {@inheritDoc} */
483     protected SAMLMessageDecoder getInboundMessageDecoder(BaseSAMLProfileRequestContext requestContext)
484             throws ProfileException {
485         return messageDecoder;
486     }
487 }