2 * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package edu.internet2.middleware.shibboleth.idp.authn;
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;
25 import java.util.Map.Entry;
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;
36 import org.joda.time.DateTime;
37 import org.opensaml.xml.util.DatatypeHelper;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
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;
51 * Manager responsible for handling authentication requests.
53 public class AuthenticationEngine extends HttpServlet {
55 /** Name of the IdP Cookie containing the IdP session ID. */
56 public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
58 /** Serial version UID. */
59 private static final long serialVersionUID = 8494202791991613148L;
62 private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
65 * Gets the manager used to retrieve handlers for requests.
67 * @return manager used to retrieve handlers for requests
69 public IdPProfileHandlerManager getProfileHandlerManager() {
70 return (IdPProfileHandlerManager) getServletContext().getAttribute("handlerManager");
74 * Gets the session manager to be used.
76 * @return session manager to be used
78 @SuppressWarnings("unchecked")
79 public SessionManager<Session> getSessionManager() {
80 return (SessionManager<Session>) getServletContext().getAttribute("sessionManager");
84 * Returns control back to the authentication engine.
86 * @param httpRequest current http request
87 * @param httpResponse current http response
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");
96 forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
100 * Returns control back to the profile handler that invoked the authentication engine.
102 * @param loginContext current login context
103 * @param httpRequest current http request
104 * @param httpResponse current http response
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);
113 * Forwards a request to the given path.
115 * @param forwardPath path to forward the request to
116 * @param httpRequest current HTTP request
117 * @param httpResponse current HTTP response
119 protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
120 HttpServletResponse httpResponse) {
122 RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
123 dispatcher.forward(httpRequest, httpResponse);
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);
133 @SuppressWarnings("unchecked")
134 protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
136 LOG.debug("Processing incoming request");
138 if (httpResponse.isCommitted()) {
139 LOG.error("HTTP Response already committed");
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");
149 if (!loginContext.getAuthenticationAttempted()) {
150 startUserAuthentication(loginContext, httpRequest, httpResponse);
152 completeAuthenticationWithoutActiveMethod(loginContext, httpRequest, httpResponse);
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.
161 * @param loginContext current login context
162 * @param httpRequest current HTTP request
163 * @param httpResponse current HTTP response
165 protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
166 HttpServletResponse httpResponse) {
167 LOG.debug("Beginning user authentication process");
169 Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(loginContext);
170 ArrayList<AuthenticationMethodInformation> activeAuthnMethods = new ArrayList<AuthenticationMethodInformation>();
172 Session userSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
173 if (userSession != null) {
174 activeAuthnMethods.addAll(userSession.getAuthenticationMethods().values());
177 if (loginContext.isForceAuthRequired()) {
178 LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
179 filterByForceAuthentication(loginContext, activeAuthnMethods, possibleLoginHandlers);
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);
192 if (loginContext.isPassiveAuthRequired()) {
193 LOG.debug("Passive authentication is required, filtering poassibl login handlers accordingly.");
194 filterByPassiveAuthentication(loginContext, possibleLoginHandlers);
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()
201 authenticateUser(chosenLoginHandler.getKey(), chosenLoginHandler.getValue(), loginContext, httpRequest,
203 } catch (AuthenticationException e) {
204 loginContext.setAuthenticationFailure(e);
205 returnToProfileHandler(loginContext, httpRequest, httpResponse);
211 * Determines which configured login handlers will support the requested authentication methods.
213 * @param loginContext current login context
215 * @return login methods that may be used to authenticate the user
217 * @throws AuthenticationException thrown if no login handler meets the given requirements
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());
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();
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");
242 return supportedLoginHandlers;
246 * Filters out any login handler based on the requirement for forced authentication.
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.
251 * @param loginContext current login context
252 * @param activeAuthnMethods currently active authentication methods
253 * @param loginHandlers login handlers to filter
255 * @throws ForceAuthenticationException thrown if no handlers remain after filtering
257 protected void filterByForceAuthentication(LoginContext loginContext,
258 Collection<AuthenticationMethodInformation> activeAuthnMethods, Map<String, LoginHandler> loginHandlers)
259 throws ForceAuthenticationException {
261 LoginHandler loginHandler;
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);
274 if (loginHandlers.isEmpty()) {
275 LOG.error("Force authentication required but no login handlers available to support it");
276 throw new ForceAuthenticationException();
281 * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
282 * authentication is required.
284 * @param loginContext current login context
285 * @param loginHandlers login handlers to filter
287 * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
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();
300 if (loginHandlers.isEmpty()) {
301 LOG.error("Passive authentication required but no login handlers available to support it");
302 throw new PassiveAuthenticationException();
307 * Authenticates the user with the given authentication method provided by the given login handler.
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
315 protected void authenticateUser(String authnMethod, LoginHandler logingHandler, LoginContext loginContext,
316 HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
318 loginContext.setAuthenticationAttempted();
319 loginContext.setAuthenticationDuration(logingHandler.getAuthenticationDuration());
320 loginContext.setAuthenticationMethod(authnMethod);
321 loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
322 logingHandler.login(httpRequest, httpResponse);
326 * Completes the authentication request using an existing, active, authentication method for the current user.
328 * @param authenticationMethod authentication method to use to complete the request
329 * @param httpRequest current HTTP request
330 * @param httpResponse current HTTP response
332 protected void completeAuthenticationWithActiveMethod(AuthenticationMethodInformation authenticationMethod,
333 HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
334 HttpSession httpSession = httpRequest.getSession();
336 Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
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());
346 ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
347 authenticationMethod);
348 shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
350 returnToProfileHandler(loginContext, httpRequest, httpResponse);
354 * Completes the authentication process when and already active authentication mechanism wasn't used, that is, when
355 * the user was really authenticated.
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.
361 * @param loginContext current login context
362 * @param httpRequest current HTTP request
363 * @param httpResponse current HTTP response
365 protected void completeAuthenticationWithoutActiveMethod(LoginContext loginContext, HttpServletRequest httpRequest,
366 HttpServletResponse httpResponse) {
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);
380 loginContext.setPrincipalAuthenticated(true);
381 loginContext.setPrincipalName(principalName);
382 loginContext.setAuthenticationInstant(new DateTime());
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);
391 updateUserSession(loginContext, httpRequest, httpResponse);
393 LOG.debug("User {} authentication with authentication method {}", loginContext.getPrincipalName(), loginContext
394 .getAuthenticationMethod());
396 returnToProfileHandler(loginContext, httpRequest, httpResponse);
400 * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
403 * @param loginContext current login context
404 * @param httpRequest current HTTP request
405 * @param httpResponse current HTTP response
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);
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();
425 AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
426 new DateTime(), loginContext.getAuthenticationDuration());
428 shibSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
430 ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
432 shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
436 * Adds an IdP session cookie to the outbound response.
438 * @param httpRequest current request
439 * @param httpResponse current response
440 * @param userSession user's session
442 protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
443 Session userSession) {
444 httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
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);
451 int maxAge = (int) (userSession.getInactivityTimeout() / 1000);
452 sessionCookie.setMaxAge(maxAge);
454 httpResponse.addCookie(sessionCookie);