57e0e3618f2ef207f44cf20e2f70e768becec2b1
[java-idp.git] / src / 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.io.UnsupportedEncodingException;
21 import java.net.URLDecoder;
22 import java.util.ArrayList;
23
24 import javax.servlet.RequestDispatcher;
25 import javax.servlet.ServletException;
26 import javax.servlet.ServletRequest;
27 import javax.servlet.ServletResponse;
28 import javax.servlet.http.HttpServletRequest;
29 import javax.servlet.http.HttpServletResponse;
30 import javax.servlet.http.HttpSession;
31
32 import org.apache.log4j.Logger;
33 import org.opensaml.common.SAMLObject;
34 import org.opensaml.common.SAMLObjectBuilder;
35 import org.opensaml.common.binding.BindingException;
36 import org.opensaml.common.binding.encoding.MessageEncoder;
37 import org.opensaml.common.xml.SAMLConstants;
38 import org.opensaml.saml1.core.AuthenticationStatement;
39 import org.opensaml.saml1.core.Response;
40 import org.opensaml.saml1.core.Statement;
41 import org.opensaml.saml1.core.StatusCode;
42 import org.opensaml.saml1.core.Subject;
43 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
44 import org.opensaml.xml.util.DatatypeHelper;
45
46 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
47 import edu.internet2.middleware.shibboleth.common.profile.ProfileRequest;
48 import edu.internet2.middleware.shibboleth.common.profile.ProfileResponse;
49 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
50 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml1.ShibbolethSSOConfiguration;
51 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
52 import edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext;
53
54 /** Shibboleth SSO request profile handler. */
55 public class ShibbolethSSOProfileHandler extends AbstractSAML1ProfileHandler {
56
57     /** Class logger. */
58     private final Logger log = Logger.getLogger(ShibbolethSSOProfileHandler.class);
59
60     /** Builder of AuthenticationStatement objects. */
61     private SAMLObjectBuilder<AuthenticationStatement> authnStatementBuilder;
62
63     /** URL of the authentication manager servlet. */
64     private String authenticationManagerPath;
65
66     /**
67      * Constructor.
68      * 
69      * @param authnManagerPath path to the authentication manager servlet
70      * @param encoder URI of the encoding binding
71      * 
72      * @throws IllegalArgumentException thrown if either the authentication manager path or encoding binding URI are
73      *             null or empty
74      */
75     public ShibbolethSSOProfileHandler(String authnManagerPath) {
76         if (DatatypeHelper.isEmpty(authnManagerPath)) {
77             throw new IllegalArgumentException("Authentication manager path may not be null");
78         }
79
80         authenticationManagerPath = authnManagerPath;
81
82         authnStatementBuilder = (SAMLObjectBuilder<AuthenticationStatement>) getBuilderFactory().getBuilder(
83                 AuthenticationStatement.DEFAULT_ELEMENT_NAME);
84     }
85
86     /**
87      * Convenience method for getting the SAML 1 AuthenticationStatement builder.
88      * 
89      * @return SAML 1 AuthenticationStatement builder
90      */
91     public SAMLObjectBuilder<AuthenticationStatement> getAuthenticationStatementBuilder() {
92         return authnStatementBuilder;
93     }
94
95     /** {@inheritDoc} */
96     public String getProfileId() {
97         return "urn:mace:shibboleth:2.0:idp:profiles:shibboleth:request:sso";
98     }
99
100     /** {@inheritDoc} */
101     public void processRequest(ProfileRequest<ServletRequest> request, ProfileResponse<ServletResponse> response)
102             throws ProfileException {
103
104         HttpSession httpSession = ((HttpServletRequest) request.getRawRequest()).getSession(true);
105         if (httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY) == null) {
106             performAuthentication(request, response);
107         } else {
108             completeAuthenticationRequest(request, response);
109         }
110     }
111
112     /**
113      * Creates a {@link LoginContext} an sends the request off to the AuthenticationManager to begin the process of
114      * authenticating the user.
115      * 
116      * @param request current request
117      * @param response current response
118      * 
119      * @throws ProfileException thrown if there is a problem creating the login context and transferring control to the
120      *             authentication manager
121      */
122     protected void performAuthentication(ProfileRequest<ServletRequest> request,
123             ProfileResponse<ServletResponse> response) throws ProfileException {
124
125         HttpServletRequest httpRequest = (HttpServletRequest) request.getRawRequest();
126         HttpServletResponse httpResponse = (HttpServletResponse) response.getRawResponse();
127         HttpSession httpSession = httpRequest.getSession(true);
128
129         LoginContext loginContext = buildLoginContext(httpRequest);
130         httpSession.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
131
132         try {
133             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(authenticationManagerPath);
134             dispatcher.forward(httpRequest, httpResponse);
135         } catch (IOException ex) {
136             log.error("Error forwarding Shibboleth SSO request to AuthenticationManager", ex);
137             throw new ProfileException("Error forwarding Shibboleth SSO request to AuthenticationManager", ex);
138         } catch (ServletException ex) {
139             log.error("Error forwarding Shibboleth SSO request to AuthenticationManager", ex);
140             throw new ProfileException("Error forwarding Shibboleth SSO request to AuthenticationManager", ex);
141         }
142     }
143
144     /**
145      * Creates a response to the Shibboleth SSO and sends the user, with response in tow, back to the relying party
146      * after they've been authenticated.
147      * 
148      * @param request current request
149      * @param response current response
150      * 
151      * @throws ProfileException thrown if the response can not be created and sent back to the relying party
152      */
153     protected void completeAuthenticationRequest(ProfileRequest<ServletRequest> request,
154             ProfileResponse<ServletResponse> response) throws ProfileException {
155         HttpSession httpSession = ((HttpServletRequest) request.getRawRequest()).getSession(true);
156
157         ShibbolethSSOLoginContext loginContext = (ShibbolethSSOLoginContext) httpSession
158                 .getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
159         httpSession.removeAttribute(LoginContext.LOGIN_CONTEXT_KEY);
160
161         ShibbolethSSORequestContext requestContext = buildRequestContext(loginContext, request, response);
162
163         Response samlResponse;
164         try {
165             if (!loginContext.isPrincipalAuthenticated()) {
166                 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER, null, "User failed authentication"));
167                 throw new ProfileException("User failed authentication");
168             }
169
170             ArrayList<Statement> statements = new ArrayList<Statement>();
171             statements.add(buildAuthenticationStatement(requestContext));
172             if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
173                 statements
174                         .add(buildAttributeStatement(requestContext, "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches"));
175             }
176
177             samlResponse = buildResponse(requestContext, statements);
178         } catch (ProfileException e) {
179             samlResponse = buildErrorResponse(requestContext);
180         }
181
182         requestContext.setSamlResponse(samlResponse);
183         encodeResponse(requestContext);
184         writeAuditLogEntry(requestContext);
185     }
186
187     /**
188      * Creates a login context from the incoming HTTP request.
189      * 
190      * @param request current HTTP request
191      * 
192      * @return the constructed login context
193      * 
194      * @throws ProfileException thrown if the incomming request did not contain a providerId, shire, and target
195      *             parameter
196      */
197     protected ShibbolethSSOLoginContext buildLoginContext(HttpServletRequest request) throws ProfileException {
198         ShibbolethSSOLoginContext loginContext = new ShibbolethSSOLoginContext();
199
200         try {
201             String providerId = DatatypeHelper.safeTrimOrNullString(request.getParameter("providerId"));
202             if (providerId == null) {
203                 log.error("No providerId parameter in Shibboleth SSO request");
204                 throw new ProfileException("No providerId parameter in Shibboleth SSO request");
205             }
206             loginContext.setRelyingParty(URLDecoder.decode(providerId, "UTF-8"));
207             
208             RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(providerId);
209             if(rpConfig == null){
210                 log.error("No relying party configuration available for " + providerId);
211                 throw new ProfileException("No relying party configuration available for " + providerId);
212             }
213             loginContext.getRequestedAuthenticationMethods().add(rpConfig.getDefaultAuthenticationMethod());
214
215             String acs = DatatypeHelper.safeTrimOrNullString(request.getParameter("shire"));
216             if (acs == null) {
217                 log.error("No shire parameter in Shibboleth SSO request");
218                 throw new ProfileException("No shire parameter in Shibboleth SSO request");
219             }
220             loginContext.setSpAssertionConsumerService(URLDecoder.decode(acs, "UTF-8"));
221
222             String target = DatatypeHelper.safeTrimOrNullString(request.getParameter("target"));
223             if (target == null) {
224                 log.error("No target parameter in Shibboleth SSO request");
225                 throw new ProfileException("No target parameter in Shibboleth SSO request");
226             }
227             loginContext.setSpTarget(URLDecoder.decode(target, "UTF-8"));
228         } catch (UnsupportedEncodingException e) {
229             // UTF-8 encoding required to be supported by all JVMs.
230         }
231
232         loginContext.setAuthenticationEngineURL(authenticationManagerPath);
233         loginContext.setProfileHandlerURL(request.getRequestURI());
234         return loginContext;
235     }
236
237     /**
238      * Creates an authentication request context from the current environmental information.
239      * 
240      * @param loginContext current login context
241      * @param request current request
242      * @param response current response
243      * 
244      * @return created authentication request context
245      * 
246      * @throws ProfileException thrown if asserting and relying party metadata can not be located
247      */
248     protected ShibbolethSSORequestContext buildRequestContext(ShibbolethSSOLoginContext loginContext,
249             ProfileRequest<ServletRequest> request, ProfileResponse<ServletResponse> response) throws ProfileException {
250         ShibbolethSSORequestContext requestContext = new ShibbolethSSORequestContext(request, response);
251
252         requestContext.setLoginContext(loginContext);
253
254         requestContext.setPrincipalName(loginContext.getPrincipalName());
255
256         requestContext.setPrincipalAuthenticationMethod(loginContext.getAuthenticationMethod());
257
258         String relyingPartyId = loginContext.getRelyingPartyId();
259
260         requestContext.setRelyingPartyId(relyingPartyId);
261
262         try {
263
264             requestContext.setRelyingPartyMetadata(getMetadataProvider().getEntityDescriptor(
265                     requestContext.getRelyingPartyId()));
266
267             requestContext.setRelyingPartyRoleMetadata(requestContext.getRelyingPartyMetadata().getSPSSODescriptor(
268                     SAMLConstants.SAML1P_NS));
269
270             RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
271             requestContext.setRelyingPartyConfiguration(rpConfig);
272
273             requestContext.setAssertingPartyId(requestContext.getRelyingPartyConfiguration().getProviderId());
274
275             requestContext.setAssertingPartyMetadata(getMetadataProvider().getEntityDescriptor(
276                     requestContext.getAssertingPartyId()));
277
278             requestContext.setAssertingPartyRoleMetadata(requestContext.getAssertingPartyMetadata()
279                     .getIDPSSODescriptor(SAMLConstants.SAML1P_NS));
280
281             requestContext.setProfileConfiguration((ShibbolethSSOConfiguration) rpConfig
282                     .getProfileConfiguration(ShibbolethSSOConfiguration.PROFILE_ID));
283
284             return requestContext;
285         } catch (MetadataProviderException e) {
286             log.error("Unable to locate metadata for asserting or relying party");
287             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER, null, "Error locating party metadata"));
288             throw new ProfileException("Error locating party metadata");
289         }
290     }
291
292     /**
293      * Builds the authentication statement for the authenticated principal.
294      * 
295      * @param requestContext current request context
296      * 
297      * @return the created statement
298      * 
299      * @throws ProfileException thrown if the authentication statement can not be created
300      */
301     protected AuthenticationStatement buildAuthenticationStatement(ShibbolethSSORequestContext requestContext)
302             throws ProfileException {
303         ShibbolethSSOLoginContext loginContext = requestContext.getLoginContext();
304
305         AuthenticationStatement statement = getAuthenticationStatementBuilder().buildObject();
306         statement.setAuthenticationInstant(loginContext.getAuthenticationInstant());
307         statement.setAuthenticationMethod(loginContext.getAuthenticationMethod());
308
309         // TODO
310         statement.setSubjectLocality(null);
311
312         Subject statementSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches");
313         statement.setSubject(statementSubject);
314
315         return statement;
316     }
317
318     /**
319      * Encodes the request's SAML response and writes it to the servlet response.
320      * 
321      * @param requestContext current request context
322      * 
323      * @throws ProfileException thrown if no message encoder is registered for this profiles binding
324      */
325     protected void encodeResponse(ShibbolethSSORequestContext requestContext) throws ProfileException {
326         if (log.isDebugEnabled()) {
327             log.debug("Encoding response to SAML request from relying party " + requestContext.getRelyingPartyId());
328         }
329
330         
331         //TODO endpoint selection
332         MessageEncoder<ServletResponse> encoder = null;
333
334         super.populateMessageEncoder(encoder);
335         ProfileResponse<ServletResponse> profileResponse = requestContext.getProfileResponse(); 
336         encoder.setResponse(profileResponse.getRawResponse());
337         encoder.setSamlMessage(requestContext.getSamlResponse());
338         requestContext.setMessageEncoder(encoder);
339
340         try {
341             encoder.encode();
342         } catch (BindingException e) {
343             throw new ProfileException("Unable to encode response to relying party: "
344                     + requestContext.getRelyingPartyId(), e);
345         }
346     }
347
348     /** Represents the internal state of a Shibboleth SSO Request while it's being processed by the IdP. */
349     protected class ShibbolethSSORequestContext extends
350             SAML1ProfileRequestContext<SAMLObject, Response, ShibbolethSSOConfiguration> {
351
352         /** Current login context. */
353         private ShibbolethSSOLoginContext loginContext;
354
355         /**
356          * Constructor.
357          * 
358          * @param request current profile request
359          * @param response current profile response
360          */
361         public ShibbolethSSORequestContext(ProfileRequest<ServletRequest> request,
362                 ProfileResponse<ServletResponse> response) {
363             super(request, response);
364         }
365
366         /**
367          * Gets the current login context.
368          * 
369          * @return current login context
370          */
371         public ShibbolethSSOLoginContext getLoginContext() {
372             return loginContext;
373         }
374
375         /**
376          * Sets the current login context.
377          * 
378          * @param context current login context
379          */
380         public void setLoginContext(ShibbolethSSOLoginContext context) {
381             loginContext = context;
382         }
383     }
384 }