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