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