View Javadoc

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