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