Be sure to reattach login context to request when sending it back to the profile...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / authn / AuthenticationEngine.java
1 /*
2  * Copyright [2006] [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.authn;
18
19 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.Map;
25 import java.util.Map.Entry;
26
27 import javax.security.auth.Subject;
28 import javax.servlet.RequestDispatcher;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.Cookie;
31 import javax.servlet.http.HttpServlet;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34 import javax.servlet.http.HttpSession;
35
36 import org.joda.time.DateTime;
37 import org.opensaml.xml.util.DatatypeHelper;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
42 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
43 import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
44 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
45 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
46 import edu.internet2.middleware.shibboleth.idp.session.Session;
47 import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
48 import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
49
50 /**
51  * Manager responsible for handling authentication requests.
52  */
53 public class AuthenticationEngine extends HttpServlet {
54
55     /** Name of the IdP Cookie containing the IdP session ID. */
56     public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
57
58     /** Serial version UID. */
59     private static final long serialVersionUID = 8494202791991613148L;
60
61     /** Class logger. */
62     private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
63
64     /**
65      * Gets the manager used to retrieve handlers for requests.
66      * 
67      * @return manager used to retrieve handlers for requests
68      */
69     public IdPProfileHandlerManager getProfileHandlerManager() {
70         return (IdPProfileHandlerManager) getServletContext().getAttribute("handlerManager");
71     }
72
73     /**
74      * Gets the session manager to be used.
75      * 
76      * @return session manager to be used
77      */
78     @SuppressWarnings("unchecked")
79     public SessionManager<Session> getSessionManager() {
80         return (SessionManager<Session>) getServletContext().getAttribute("sessionManager");
81     }
82
83     /**
84      * Returns control back to the authentication engine.
85      * 
86      * @param httpRequest current http request
87      * @param httpResponse current http response
88      */
89     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
90         LOG.debug("Returning control to authentication engine");
91         HttpSession httpSession = httpRequest.getSession();
92         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
93         if (loginContext == null) {
94             LOG.error("User HttpSession did not contain a login context.  Unable to return to authentication engine");
95         }
96         forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
97     }
98
99     /**
100      * Returns control back to the profile handler that invoked the authentication engine.
101      * 
102      * @param loginContext current login context
103      * @param httpRequest current http request
104      * @param httpResponse current http response
105      */
106     public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
107             HttpServletResponse httpResponse) {
108         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
109         httpRequest.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
110         forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
111     }
112
113     /**
114      * Forwards a request to the given path.
115      * 
116      * @param forwardPath path to forward the request to
117      * @param httpRequest current HTTP request
118      * @param httpResponse current HTTP response
119      */
120     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
121             HttpServletResponse httpResponse) {
122         try {
123             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
124             dispatcher.forward(httpRequest, httpResponse);
125             return;
126         } catch (IOException e) {
127             LOG.error("Unable to return control back to authentication engine", e);
128         } catch (ServletException e) {
129             LOG.error("Unable to return control back to authentication engine", e);
130         }
131     }
132
133     /** {@inheritDoc} */
134     @SuppressWarnings("unchecked")
135     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
136             IOException {
137         LOG.debug("Processing incoming request");
138
139         if (httpResponse.isCommitted()) {
140             LOG.error("HTTP Response already committed");
141         }
142
143         LoginContext loginContext = (LoginContext) httpRequest.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
144         if (loginContext == null) {
145             // When the login context comes from the profile handlers its attached to the request
146             // The authn engine attaches it to the session to allow the handlers to do any number of
147             // request/response pairs without maintaining or losing the login context
148             loginContext = (LoginContext) httpRequest.getSession().getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
149         } else {
150             // Clean out any old state that might be lying around
151             httpRequest.getSession().removeAttribute(LoginContext.LOGIN_CONTEXT_KEY);
152         }
153
154         if (loginContext == null) {
155             LOG.error("Incoming request does not have attached login context");
156             throw new ServletException("Incoming request does not have attached login context");
157         }
158
159         if (!loginContext.getAuthenticationAttempted()) {
160             startUserAuthentication(loginContext, httpRequest, httpResponse);
161         } else {
162             completeAuthenticationWithoutActiveMethod(loginContext, httpRequest, httpResponse);
163         }
164     }
165
166     /**
167      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
168      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
169      * depending on whether passive authentication is required.
170      * 
171      * @param loginContext current login context
172      * @param httpRequest current HTTP request
173      * @param httpResponse current HTTP response
174      */
175     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
176             HttpServletResponse httpResponse) {
177         LOG.debug("Beginning user authentication process");
178         try {
179             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(loginContext);
180             ArrayList<AuthenticationMethodInformation> activeAuthnMethods = new ArrayList<AuthenticationMethodInformation>();
181
182             Session userSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
183             if (userSession != null) {
184                 activeAuthnMethods.addAll(userSession.getAuthenticationMethods().values());
185             }
186
187             if (loginContext.isForceAuthRequired()) {
188                 LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
189                 filterByForceAuthentication(loginContext, activeAuthnMethods, possibleLoginHandlers);
190             } else {
191                 LOG.debug("Forced authentication not required, trying existing authentication methods");
192                 for (AuthenticationMethodInformation activeAuthnMethod : activeAuthnMethods) {
193                     if (possibleLoginHandlers.containsKey(activeAuthnMethod.getAuthenticationMethod())) {
194                         completeAuthenticationWithActiveMethod(loginContext, activeAuthnMethod, httpRequest,
195                                 httpResponse);
196                         return;
197                     }
198                 }
199                 LOG.debug("No existing authentication method meets service provides requirements");
200             }
201
202             if (loginContext.isPassiveAuthRequired()) {
203                 LOG.debug("Passive authentication is required, filtering poassibl login handlers accordingly.");
204                 filterByPassiveAuthentication(loginContext, possibleLoginHandlers);
205             }
206
207             // Since we made it this far, just pick the first remaining login handler from the list
208             Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
209             LOG.debug("Authenticating user with login handler of type {}", chosenLoginHandler.getValue().getClass()
210                     .getName());
211             authenticateUser(chosenLoginHandler.getKey(), chosenLoginHandler.getValue(), loginContext, httpRequest,
212                     httpResponse);
213         } catch (AuthenticationException e) {
214             loginContext.setAuthenticationFailure(e);
215             returnToProfileHandler(loginContext, httpRequest, httpResponse);
216         }
217
218     }
219
220     /**
221      * Determines which configured login handlers will support the requested authentication methods.
222      * 
223      * @param loginContext current login context
224      * 
225      * @return login methods that may be used to authenticate the user
226      * 
227      * @throws AuthenticationException thrown if no login handler meets the given requirements
228      */
229     protected Map<String, LoginHandler> determinePossibleLoginHandlers(LoginContext loginContext)
230             throws AuthenticationException {
231         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(getProfileHandlerManager()
232                 .getLoginHandlers());
233         LOG.trace("Supported login handlers: {}", supportedLoginHandlers);
234         LOG.trace("Requested authentication methods: {}", loginContext.getRequestedAuthenticationMethods());
235
236         Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet().iterator();
237         Entry<String, LoginHandler> supportedLoginHandler;
238         while (supportedLoginHandlerItr.hasNext()) {
239             supportedLoginHandler = supportedLoginHandlerItr.next();
240             if (!loginContext.getRequestedAuthenticationMethods().contains(supportedLoginHandler.getKey())) {
241                 supportedLoginHandlerItr.remove();
242                 continue;
243             }
244         }
245
246         if (supportedLoginHandlers.isEmpty()) {
247             LOG.error("No authentication method, requested by the service provider, is supported");
248             throw new AuthenticationException(
249                     "No authentication method, requested by the service provider, is supported");
250         }
251
252         return supportedLoginHandlers;
253     }
254
255     /**
256      * Filters out any login handler based on the requirement for forced authentication.
257      * 
258      * During forced authentication any handler that has not previously been used to authenticate the the user or any
259      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
260      * 
261      * @param loginContext current login context
262      * @param activeAuthnMethods currently active authentication methods, never null
263      * @param loginHandlers login handlers to filter
264      * 
265      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
266      */
267     protected void filterByForceAuthentication(LoginContext loginContext,
268             Collection<AuthenticationMethodInformation> activeAuthnMethods, Map<String, LoginHandler> loginHandlers)
269             throws ForceAuthenticationException {
270
271         LoginHandler loginHandler;
272         for (AuthenticationMethodInformation activeAuthnMethod : activeAuthnMethods) {
273             loginHandler = loginHandlers.get(activeAuthnMethod.getAuthenticationMethod());
274             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
275                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
276                     loginHandlers.remove(handlerSupportedMethods);
277                 }
278             }
279         }
280
281         if (loginHandlers.isEmpty()) {
282             LOG.error("Force authentication required but no login handlers available to support it");
283             throw new ForceAuthenticationException();
284         }
285     }
286
287     /**
288      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
289      * authentication is required.
290      * 
291      * @param loginContext current login context
292      * @param loginHandlers login handlers to filter
293      * 
294      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
295      */
296     protected void filterByPassiveAuthentication(LoginContext loginContext, Map<String, LoginHandler> loginHandlers)
297             throws PassiveAuthenticationException {
298         LoginHandler loginHandler;
299         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
300         while (authnMethodItr.hasNext()) {
301             loginHandler = authnMethodItr.next().getValue();
302             if (!loginHandler.supportsPassive()) {
303                 authnMethodItr.remove();
304             }
305         }
306
307         if (loginHandlers.isEmpty()) {
308             LOG.error("Passive authentication required but no login handlers available to support it");
309             throw new PassiveAuthenticationException();
310         }
311     }
312
313     /**
314      * Authenticates the user with the given authentication method provided by the given login handler.
315      * 
316      * @param authnMethod the authentication method that will be used to authenticate the user
317      * @param logingHandler login handler that will authenticate user
318      * @param loginContext current login context
319      * @param httpRequest current HTTP request
320      * @param httpResponse current HTTP response
321      */
322     protected void authenticateUser(String authnMethod, LoginHandler logingHandler, LoginContext loginContext,
323             HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
324
325         loginContext.setAuthenticationAttempted();
326         loginContext.setAuthenticationDuration(logingHandler.getAuthenticationDuration());
327         loginContext.setAuthenticationMethod(authnMethod);
328         loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
329         logingHandler.login(httpRequest, httpResponse);
330         httpRequest.getSession().setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
331     }
332
333     /**
334      * Completes the authentication request using an existing, active, authentication method for the current user.
335      * 
336      * @param loginContext current login context
337      * @param authenticationMethod authentication method to use to complete the request
338      * @param httpRequest current HTTP request
339      * @param httpResponse current HTTP response
340      */
341     protected void completeAuthenticationWithActiveMethod(LoginContext loginContext,
342             AuthenticationMethodInformation authenticationMethod, HttpServletRequest httpRequest,
343             HttpServletResponse httpResponse) {
344         Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
345
346         loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
347         loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
348         loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
349         loginContext.setPrincipalAuthenticated(true);
350         loginContext.setPrincipalName(shibSession.getPrincipalName());
351
352         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
353                 authenticationMethod);
354         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
355
356         LOG.debug("Treating user {} as authenticated via existing method {}", loginContext.getPrincipalName(),
357                 loginContext.getAuthenticationMethod());
358         returnToProfileHandler(loginContext, httpRequest, httpResponse);
359     }
360
361     /**
362      * Completes the authentication process when and already active authentication mechanism wasn't used, that is, when
363      * the user was really authenticated.
364      * 
365      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
366      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
367      * recorded and finally control is returned back to the profile handler.
368      * 
369      * @param loginContext current login context
370      * @param httpRequest current HTTP request
371      * @param httpResponse current HTTP response
372      */
373     protected void completeAuthenticationWithoutActiveMethod(LoginContext loginContext, HttpServletRequest httpRequest,
374             HttpServletResponse httpResponse) {
375
376         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
377                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
378         if (principalName == null) {
379             loginContext.setPrincipalAuthenticated(false);
380             loginContext.setAuthenticationFailure(new AuthenticationException(
381                     "No principal name returned from authentication handler."));
382             LOG.error("No principal name returned from authentication method: "
383                     + loginContext.getAuthenticationMethod());
384             returnToProfileHandler(loginContext, httpRequest, httpResponse);
385             return;
386         }
387
388         loginContext.setPrincipalAuthenticated(true);
389         loginContext.setPrincipalName(principalName);
390         loginContext.setAuthenticationInstant(new DateTime());
391
392         // We allow a login handler to override the authentication method in the event that it supports multiple methods
393         String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
394                 .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
395         if (actualAuthnMethod != null) {
396             loginContext.setAuthenticationMethod(actualAuthnMethod);
397         }
398
399         updateUserSession(loginContext, httpRequest, httpResponse);
400
401         LOG.debug("User {} authentication with authentication method {}", loginContext.getPrincipalName(), loginContext
402                 .getAuthenticationMethod());
403         returnToProfileHandler(loginContext, httpRequest, httpResponse);
404     }
405
406     /**
407      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
408      * created.
409      * 
410      * @param loginContext current login context
411      * @param httpRequest current HTTP request
412      * @param httpResponse current HTTP response
413      */
414     protected void updateUserSession(LoginContext loginContext, HttpServletRequest httpRequest,
415             HttpServletResponse httpResponse) {
416         Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
417         if (shibSession == null) {
418             LOG.debug("Creating shibboleth session for principal {}", loginContext.getPrincipalName());
419             shibSession = (Session) getSessionManager().createSession(loginContext.getPrincipalName());
420             loginContext.setSessionID(shibSession.getSessionID());
421             addSessionCookie(httpRequest, httpResponse, shibSession);
422         }
423
424         LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
425                 loginContext.getPrincipalName());
426         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
427         String authnMethod = (String) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY);
428         if (DatatypeHelper.isEmpty(authnMethod)) {
429             authnMethod = loginContext.getAuthenticationMethod();
430         }
431
432         AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
433                 new DateTime(), loginContext.getAuthenticationDuration());
434
435         shibSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
436
437         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
438                 authnMethodInfo);
439         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
440     }
441
442     /**
443      * Adds an IdP session cookie to the outbound response.
444      * 
445      * @param httpRequest current request
446      * @param httpResponse current response
447      * @param userSession user's session
448      */
449     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
450             Session userSession) {
451         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
452
453         LOG.debug("Adding IdP session cookie to HTTP response");
454         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, userSession.getSessionID());
455         sessionCookie.setPath(httpRequest.getContextPath());
456         sessionCookie.setSecure(false);
457
458         int maxAge = (int) (userSession.getInactivityTimeout() / 1000);
459         sessionCookie.setMaxAge(maxAge);
460
461         httpResponse.addCookie(sessionCookie);
462     }
463 }