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