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.saml1;
19  
20  import java.io.IOException;
21  import java.util.ArrayList;
22  
23  import javax.servlet.ServletContext;
24  import javax.servlet.http.HttpServletRequest;
25  import javax.servlet.http.HttpServletResponse;
26  
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.saml1.core.AttributeStatement;
31  import org.opensaml.saml1.core.AuthenticationStatement;
32  import org.opensaml.saml1.core.Request;
33  import org.opensaml.saml1.core.Response;
34  import org.opensaml.saml1.core.Statement;
35  import org.opensaml.saml1.core.StatusCode;
36  import org.opensaml.saml1.core.Subject;
37  import org.opensaml.saml1.core.SubjectLocality;
38  import org.opensaml.saml2.metadata.AssertionConsumerService;
39  import org.opensaml.saml2.metadata.Endpoint;
40  import org.opensaml.saml2.metadata.EntityDescriptor;
41  import org.opensaml.saml2.metadata.IDPSSODescriptor;
42  import org.opensaml.saml2.metadata.SPSSODescriptor;
43  import org.opensaml.util.URLBuilder;
44  import org.opensaml.ws.message.decoder.MessageDecodingException;
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.ws.transport.http.HttpServletResponseAdapter;
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.ShibbolethConstants;
55  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
56  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
57  import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfiguration;
58  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
59  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
60  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml1.ShibbolethSSOConfiguration;
61  import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
62  import edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext;
63  import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
64  import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
65  
66  /** Shibboleth SSO request profile handler. */
67  public class ShibbolethSSOProfileHandler extends AbstractSAML1ProfileHandler {
68  
69      /** Class logger. */
70      private final Logger log = LoggerFactory.getLogger(ShibbolethSSOProfileHandler.class);
71  
72      /** Builder of AuthenticationStatement objects. */
73      private SAMLObjectBuilder<AuthenticationStatement> authnStatementBuilder;
74  
75      /** Builder of SubjectLocality objects. */
76      private SAMLObjectBuilder<SubjectLocality> subjectLocalityBuilder;
77  
78      /** Builder of Endpoint objects. */
79      private SAMLObjectBuilder<Endpoint> endpointBuilder;
80  
81      /** URL of the authentication manager servlet. */
82      private String authenticationManagerPath;
83  
84      /**
85       * Constructor.
86       * 
87       * @param authnManagerPath path to the authentication manager servlet
88       */
89      public ShibbolethSSOProfileHandler(String authnManagerPath) {
90          if (DatatypeHelper.isEmpty(authnManagerPath)) {
91              throw new IllegalArgumentException("Authentication manager path may not be null");
92          }
93          if (authnManagerPath.startsWith("/")) {
94              authenticationManagerPath = authnManagerPath;
95          } else {
96              authenticationManagerPath = "/" + authnManagerPath;
97          }
98  
99          authnStatementBuilder = (SAMLObjectBuilder<AuthenticationStatement>) getBuilderFactory().getBuilder(
100                 AuthenticationStatement.DEFAULT_ELEMENT_NAME);
101 
102         subjectLocalityBuilder = (SAMLObjectBuilder<SubjectLocality>) getBuilderFactory().getBuilder(
103                 SubjectLocality.DEFAULT_ELEMENT_NAME);
104 
105         endpointBuilder = (SAMLObjectBuilder<Endpoint>) getBuilderFactory().getBuilder(
106                 AssertionConsumerService.DEFAULT_ELEMENT_NAME);
107     }
108 
109     /** {@inheritDoc} */
110     public String getProfileId() {
111         return ShibbolethSSOConfiguration.PROFILE_ID;
112     }
113 
114     /** {@inheritDoc} */
115     public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
116         log.debug("Processing incoming request");
117 
118         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
119         HttpServletResponse httpResponse = ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
120         ServletContext servletContext = httpRequest.getSession().getServletContext();
121 
122 	LoginContext loginContext = HttpServletHelper.getLoginContext(
123                 getStorageService(), servletContext, httpRequest);
124 
125         if (loginContext == null || !(loginContext instanceof ShibbolethSSOLoginContext)) {
126             log.debug("Incoming request does not contain a login context, processing as first leg of request");
127             performAuthentication(inTransport, outTransport);
128         } else if (loginContext.isPrincipalAuthenticated() || loginContext.getAuthenticationFailure() != null) {
129             log.debug("Incoming request contains a login context, processing as second leg of request");
130             HttpServletHelper.unbindLoginContext(getStorageService(), servletContext, httpRequest, httpResponse);
131             completeAuthenticationRequest((ShibbolethSSOLoginContext)loginContext, inTransport, outTransport);
132         } else {
133             log.debug("Incoming request contained a login context but principal was not authenticated, processing as first leg of request");
134             performAuthentication(inTransport, outTransport);
135         }
136     }
137 
138     /**
139      * Creates a {@link ShibbolethSSOLoginContext} an sends the request off to the AuthenticationManager to begin the
140      * process of authenticating the user.
141      * 
142      * @param inTransport inbound message transport
143      * @param outTransport outbound message transport
144      * 
145      * @throws ProfileException thrown if there is a problem creating the login context and transferring control to the
146      *             authentication manager
147      */
148     protected void performAuthentication(HTTPInTransport inTransport, HTTPOutTransport outTransport)
149             throws ProfileException {
150 
151         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
152         HttpServletResponse httpResponse = ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
153         ShibbolethSSORequestContext requestContext = new ShibbolethSSORequestContext();
154 
155         decodeRequest(requestContext, inTransport, outTransport);
156         ShibbolethSSOLoginContext loginContext = requestContext.getLoginContext();
157 
158         RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(loginContext.getRelyingPartyId());
159         loginContext.setDefaultAuthenticationMethod(rpConfig.getDefaultAuthenticationMethod());
160         ProfileConfiguration ssoConfig = rpConfig.getProfileConfiguration(ShibbolethSSOConfiguration.PROFILE_ID);
161         if (ssoConfig == null) {
162             String msg = "Shibboleth SSO profile is not configured for relying party "
163                     + loginContext.getRelyingPartyId();
164             log.warn(msg);
165             throw new ProfileException(msg);
166         }
167 
168         HttpServletHelper.bindLoginContext(loginContext, getStorageService(), httpRequest.getSession()
169                 .getServletContext(), httpRequest, httpResponse);
170 
171         try {
172             String authnEngineUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, authenticationManagerPath)
173                     .buildURL();
174             log.debug("Redirecting user to authentication engine at {}", authnEngineUrl);
175             httpResponse.sendRedirect(authnEngineUrl);
176         } catch (IOException e) {
177             String msg = "Error forwarding Shibboleth SSO request to AuthenticationManager";
178             log.error(msg, e);
179             throw new ProfileException(msg, e);
180         }
181     }
182 
183     /**
184      * Decodes an incoming request and populates a created request context with the resultant information.
185      * 
186      * @param inTransport inbound message transport
187      * @param outTransport outbound message transport
188      * @param requestContext the request context to which decoded information should be added
189      * 
190      * @throws ProfileException throw if there is a problem decoding the request
191      */
192     protected void decodeRequest(ShibbolethSSORequestContext requestContext, HTTPInTransport inTransport,
193             HTTPOutTransport outTransport) throws ProfileException {
194         if (log.isDebugEnabled()) {
195             log.debug("Decoding message with decoder binding {}", getInboundMessageDecoder(requestContext)
196                     .getBindingURI());
197         }
198 
199         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
200 
201         requestContext.setCommunicationProfileId(getProfileId());
202 
203         requestContext.setMetadataProvider(getMetadataProvider());
204         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
205 
206         requestContext.setCommunicationProfileId(ShibbolethSSOConfiguration.PROFILE_ID);
207         requestContext.setInboundMessageTransport(inTransport);
208         requestContext.setInboundSAMLProtocol(ShibbolethConstants.SHIB_SSO_PROFILE_URI);
209         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
210 
211         requestContext.setOutboundMessageTransport(outTransport);
212         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML11P_NS);
213 
214         SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
215         requestContext.setMessageDecoder(decoder);
216         try {
217             decoder.decode(requestContext);
218             log.debug("Decoded Shibboleth SSO request from relying party '{}'",
219                     requestContext.getInboundMessageIssuer());
220         } catch (MessageDecodingException e) {
221             String msg = "Error decoding Shibboleth SSO request";
222             log.warn(msg, e);
223             throw new ProfileException(msg, e);
224         } catch (SecurityException e) {
225             String msg = "Shibboleth SSO request does not meet security requirements: " + e.getMessage();
226             log.warn(msg);
227             throw new ProfileException(msg, e);
228         }
229 
230         ShibbolethSSOLoginContext loginContext = new ShibbolethSSOLoginContext();
231         loginContext.setRelyingParty(requestContext.getInboundMessageIssuer());
232         loginContext.setSpAssertionConsumerService(requestContext.getSpAssertionConsumerService());
233         loginContext.setSpTarget(requestContext.getRelayState());
234         loginContext.setAuthenticationEngineURL(authenticationManagerPath);
235         loginContext.setProfileHandlerURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
236         requestContext.setLoginContext(loginContext);
237     }
238 
239     /**
240      * Creates a response to the Shibboleth SSO and sends the user, with response in tow, back to the relying party
241      * after they've been authenticated.
242      * 
243      * @param loginContext login context for this request
244      * @param inTransport inbound message transport
245      * @param outTransport outbound message transport
246      * 
247      * @throws ProfileException thrown if the response can not be created and sent back to the relying party
248      */
249     protected void completeAuthenticationRequest(ShibbolethSSOLoginContext loginContext, HTTPInTransport inTransport,
250             HTTPOutTransport outTransport) throws ProfileException {
251         ShibbolethSSORequestContext requestContext = buildRequestContext(loginContext, inTransport, outTransport);
252 
253         Response samlResponse;
254         try {
255             if (loginContext.getAuthenticationFailure() != null) {
256                 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER, null, "User failed authentication"));
257                 throw new ProfileException("Authentication failure", loginContext.getAuthenticationFailure());
258             }
259 
260             resolveAttributes(requestContext);
261 
262             ArrayList<Statement> statements = new ArrayList<Statement>();
263             statements.add(buildAuthenticationStatement(requestContext));
264             if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
265                 AttributeStatement attributeStatement = buildAttributeStatement(requestContext,
266                         "urn:oasis:names:tc:SAML:1.0:cm:bearer");
267                 if (attributeStatement != null) {
268                     requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
269                     statements.add(attributeStatement);
270                 }
271             }
272 
273             samlResponse = buildResponse(requestContext, statements);
274         } catch (ProfileException e) {
275             samlResponse = buildErrorResponse(requestContext);
276         }
277 
278         requestContext.setOutboundSAMLMessage(samlResponse);
279         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
280         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
281         encodeResponse(requestContext);
282         writeAuditLogEntry(requestContext);
283     }
284 
285     /**
286      * Creates an authentication request context from the current environmental information.
287      * 
288      * @param loginContext current login context
289      * @param in inbound transport
290      * @param out outbount transport
291      * 
292      * @return created authentication request context
293      * 
294      * @throws ProfileException thrown if there is a problem creating the context
295      */
296     protected ShibbolethSSORequestContext buildRequestContext(ShibbolethSSOLoginContext loginContext,
297             HTTPInTransport in, HTTPOutTransport out) throws ProfileException {
298         ShibbolethSSORequestContext requestContext = new ShibbolethSSORequestContext();
299         requestContext.setCommunicationProfileId(getProfileId());
300 
301         requestContext.setMessageDecoder(getInboundMessageDecoder(requestContext));
302 
303         requestContext.setLoginContext(loginContext);
304         requestContext.setRelayState(loginContext.getSpTarget());
305 
306         requestContext.setInboundMessageTransport(in);
307         requestContext.setInboundSAMLProtocol(ShibbolethConstants.SHIB_SSO_PROFILE_URI);
308 
309         requestContext.setOutboundMessageTransport(out);
310         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML11P_NS);
311 
312         requestContext.setMetadataProvider(getMetadataProvider());
313 
314         String relyingPartyId = loginContext.getRelyingPartyId();
315         requestContext.setPeerEntityId(relyingPartyId);
316         requestContext.setInboundMessageIssuer(relyingPartyId);
317 
318         populateRequestContext(requestContext);
319 
320         return requestContext;
321     }
322 
323     /** {@inheritDoc} */
324     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
325             throws ProfileException {
326         super.populateRelyingPartyInformation(requestContext);
327 
328         EntityDescriptor relyingPartyMetadata = requestContext.getPeerEntityMetadata();
329         if (relyingPartyMetadata != null) {
330             requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
331             requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML11P_NS));
332         }
333     }
334 
335     /** {@inheritDoc} */
336     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
337             throws ProfileException {
338         super.populateAssertingPartyInformation(requestContext);
339 
340         EntityDescriptor localEntityDescriptor = requestContext.getLocalEntityMetadata();
341         if (localEntityDescriptor != null) {
342             requestContext.setLocalEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
343             requestContext.setLocalEntityRoleMetadata(localEntityDescriptor
344                     .getIDPSSODescriptor(SAMLConstants.SAML20P_NS));
345         }
346     }
347 
348     /** {@inheritDoc} */
349     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
350         // nothing to do here
351     }
352 
353     /**
354      * Selects the appropriate endpoint for the relying party and stores it in the request context.
355      * 
356      * @param requestContext current request context
357      * 
358      * @return Endpoint selected from the information provided in the request context
359      */
360     protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) {
361         ShibbolethSSOLoginContext loginContext = ((ShibbolethSSORequestContext) requestContext).getLoginContext();
362 
363         Endpoint endpoint = null;
364         if (requestContext.getRelyingPartyConfiguration().getRelyingPartyId() == SAMLMDRelyingPartyConfigurationManager.ANONYMOUS_RP_NAME) {
365             if (loginContext.getSpAssertionConsumerService() != null) {
366                 endpoint = endpointBuilder.buildObject();
367                 endpoint.setLocation(loginContext.getSpAssertionConsumerService());
368                 endpoint.setBinding(getSupportedOutboundBindings().get(0));
369                 log.warn("Generating endpoint for anonymous relying party. ACS url {} and binding {}", new Object[] {
370                         requestContext.getInboundMessageIssuer(), endpoint.getLocation(), endpoint.getBinding(), });
371             } else {
372                 log.warn("Unable to generate endpoint for anonymous party.  No ACS url provided.");
373             }
374         } else {
375             ShibbolethSSOEndpointSelector endpointSelector = new ShibbolethSSOEndpointSelector();
376             endpointSelector.setSpAssertionConsumerService(loginContext.getSpAssertionConsumerService());
377             endpointSelector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
378             endpointSelector.setMetadataProvider(getMetadataProvider());
379             endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
380             endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
381             endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
382             endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
383             endpoint = endpointSelector.selectEndpoint();
384         }
385 
386         return endpoint;
387     }
388 
389     /**
390      * Builds the authentication statement for the authenticated principal.
391      * 
392      * @param requestContext current request context
393      * 
394      * @return the created statement
395      * 
396      * @throws ProfileException thrown if the authentication statement can not be created
397      */
398     protected AuthenticationStatement buildAuthenticationStatement(ShibbolethSSORequestContext requestContext)
399             throws ProfileException {
400         ShibbolethSSOLoginContext loginContext = requestContext.getLoginContext();
401 
402         AuthenticationStatement statement = authnStatementBuilder.buildObject();
403         statement.setAuthenticationInstant(loginContext.getAuthenticationInstant());
404         statement.setAuthenticationMethod(loginContext.getAuthenticationMethod());
405 
406         statement.setSubjectLocality(buildSubjectLocality(requestContext));
407 
408         Subject statementSubject;
409         Endpoint endpoint = selectEndpoint(requestContext);
410         if (endpoint.getBinding().equals(SAMLConstants.SAML1_ARTIFACT_BINDING_URI)) {
411             statementSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:1.0:cm:artifact");
412         } else {
413             statementSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:1.0:cm:bearer");
414         }
415         statement.setSubject(statementSubject);
416 
417         return statement;
418     }
419 
420     /**
421      * Constructs the subject locality for the authentication statement.
422      * 
423      * @param requestContext current request context
424      * 
425      * @return subject locality for the authentication statement
426      */
427     protected SubjectLocality buildSubjectLocality(ShibbolethSSORequestContext requestContext) {
428         SubjectLocality subjectLocality = subjectLocalityBuilder.buildObject();
429 
430         HTTPInTransport inTransport = (HTTPInTransport) requestContext.getInboundMessageTransport();
431         subjectLocality.setIPAddress(inTransport.getPeerAddress());
432 
433         return subjectLocality;
434     }
435 
436     /** Represents the internal state of a Shibboleth SSO Request while it's being processed by the IdP. */
437     public class ShibbolethSSORequestContext extends
438             BaseSAML1ProfileRequestContext<Request, Response, ShibbolethSSOConfiguration> {
439 
440         /** SP-provide assertion consumer service URL. */
441         private String spAssertionConsumerService;
442 
443         /** Current login context. */
444         private ShibbolethSSOLoginContext loginContext;
445 
446         /**
447          * Gets the current login context.
448          * 
449          * @return current login context
450          */
451         public ShibbolethSSOLoginContext getLoginContext() {
452             return loginContext;
453         }
454 
455         /**
456          * Sets the current login context.
457          * 
458          * @param context current login context
459          */
460         public void setLoginContext(ShibbolethSSOLoginContext context) {
461             loginContext = context;
462         }
463 
464         /**
465          * Gets the SP-provided assertion consumer service URL.
466          * 
467          * @return SP-provided assertion consumer service URL
468          */
469         public String getSpAssertionConsumerService() {
470             return spAssertionConsumerService;
471         }
472 
473         /**
474          * Sets the SP-provided assertion consumer service URL.
475          * 
476          * @param acs SP-provided assertion consumer service URL
477          */
478         public void setSpAssertionConsumerService(String acs) {
479             spAssertionConsumerService = acs;
480         }
481     }
482 }