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