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