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.HashMap;
22 import java.util.Iterator;
24 import java.util.Map.Entry;
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;
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.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;
52 * Manager responsible for handling authentication requests.
54 public class AuthenticationEngine extends HttpServlet {
56 /** Name of the IdP Cookie containing the IdP session ID. */
57 public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
59 /** Serial version UID. */
60 private static final long serialVersionUID = 8494202791991613148L;
63 private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
65 /** Profile handler manager. */
66 private IdPProfileHandlerManager handlerManager;
68 /** Session manager. */
69 private SessionManager<Session> sessionManager;
72 public void init(ServletConfig config) throws ServletException {
75 String handlerManagerId = config.getInitParameter("handlerManagerId");
76 if (DatatypeHelper.isEmpty(handlerManagerId)) {
77 handlerManagerId = "shibboleth.HandlerManager";
79 handlerManager = (IdPProfileHandlerManager) getServletContext().getAttribute(handlerManagerId);
81 String sessionManagerId = config.getInitParameter("sessionManagedId");
82 if (DatatypeHelper.isEmpty(sessionManagerId)) {
83 sessionManagerId = "shibboleth.SessionManager";
86 sessionManager = (SessionManager<Session>) getServletContext().getAttribute(sessionManagerId);
90 * Returns control back to the authentication engine.
92 * @param httpRequest current http request
93 * @param httpResponse current http response
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);
103 forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
108 * Returns control back to the profile handler that invoked the authentication engine.
110 * @param loginContext current login context
111 * @param httpRequest current http request
112 * @param httpResponse current http response
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);
123 * Forwards a request to the given path.
125 * @param forwardPath path to forward the request to
126 * @param httpRequest current HTTP request
127 * @param httpResponse current HTTP response
129 protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
130 HttpServletResponse httpResponse) {
132 RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
133 dispatcher.forward(httpRequest, httpResponse);
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);
143 @SuppressWarnings("unchecked")
144 protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
146 LOG.debug("Processing incoming request");
148 if (httpResponse.isCommitted()) {
149 LOG.error("HTTP Response already committed");
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);
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");
165 if (!loginContext.getAuthenticationAttempted()) {
166 startUserAuthentication(loginContext, httpRequest, httpResponse);
168 completeAuthentication(loginContext, httpRequest, httpResponse);
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.
177 * @param loginContext current login context
178 * @param httpRequest current HTTP request
179 * @param httpResponse current HTTP response
181 protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
182 HttpServletResponse httpResponse) {
183 LOG.debug("Beginning user authentication process");
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());
190 Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(loginContext);
191 LOG.debug("Possible authentication handlers for this request: {}", possibleLoginHandlers);
193 // Filter out possible candidate login handlers by forced and passive authentication requirements
194 if (loginContext.isForceAuthRequired()) {
195 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
198 if (loginContext.isPassiveAuthRequired()) {
199 filterByPassiveAuthentication(loginContext, possibleLoginHandlers);
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);
209 Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
210 authenticateUser(chosenLoginHandler.getKey(), chosenLoginHandler.getValue(), loginContext, httpRequest,
213 } catch (AuthenticationException e) {
214 loginContext.setAuthenticationFailure(e);
215 returnToProfileHandler(loginContext, httpRequest, httpResponse);
220 * Determines which configured login handlers will support the requested authentication methods.
222 * @param loginContext current login context
224 * @return login methods that may be used to authenticate the user
226 * @throws AuthenticationException thrown if no login handler meets the given requirements
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());
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();
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");
252 return supportedLoginHandlers;
256 * Filters out any login handler based on the requirement for forced authentication.
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.
261 * @param idpSession user's current IdP session
262 * @param loginContext current login context
263 * @param loginHandlers login handlers to filter
265 * @throws ForceAuthenticationException thrown if no handlers remain after filtering
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");
271 ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
272 if (idpSession != null) {
273 activeMethods.addAll(idpSession.getAuthenticationMethods().values());
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);
286 LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
289 if (loginHandlers.isEmpty()) {
290 LOG.error("Force authentication required but no login handlers available to support it");
291 throw new ForceAuthenticationException();
296 * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
297 * authentication is required.
299 * @param loginContext current login context
300 * @param loginHandlers login handlers to filter
302 * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
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.");
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();
317 LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
320 if (loginHandlers.isEmpty()) {
321 LOG.error("Passive authentication required but no login handlers available to support it");
322 throw new PassiveAuthenticationException();
327 * Completes the authentication request using an existing, active, authentication method for the current user.
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
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.");
339 Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
340 PreviousSessionLoginHandler loginHandler = (PreviousSessionLoginHandler) handlerManager.getLoginHandlers().get(
341 PreviousSessionLoginHandler.PREVIOUS_SESSION_AUTHN_METHOD);
343 AuthenticationMethodInformation authenticationMethod = null;
344 for (String possibleAuthnMethod : possibleLoginHandlers.keySet()) {
345 authenticationMethod = idpSession.getAuthenticationMethods().get(possibleAuthnMethod);
346 if (authenticationMethod != null) {
351 if (loginHandler.reportPreviousSessionAuthnMethod()) {
352 loginContext.setAuthenticationDuration(loginHandler.getAuthenticationDuration());
353 loginContext.setAuthenticationInstant(new DateTime());
354 loginContext.setAuthenticationMethod(PreviousSessionLoginHandler.PREVIOUS_SESSION_AUTHN_METHOD);
356 loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
357 loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
358 loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
360 loginContext.setPrincipalName(idpSession.getPrincipalName());
362 httpRequest.getSession().setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
363 loginHandler.login(httpRequest, httpResponse);
367 * Authenticates the user with the given authentication method provided by the given login handler.
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
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());
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);
389 * Completes the authentication process.
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.
395 * @param loginContext current login context
396 * @param httpRequest current HTTP request
397 * @param httpResponse current HTTP response
399 protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
400 HttpServletResponse httpResponse) {
401 LOG.debug("Completing user authentication process");
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);
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);
421 loginContext.setPrincipalAuthenticated(true);
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);
430 LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(), loginContext
431 .getAuthenticationMethod());
432 updateUserSession(loginContext, httpRequest, httpResponse);
433 returnToProfileHandler(loginContext, httpRequest, httpResponse);
437 * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
440 * @param loginContext current login context
441 * @param httpRequest current HTTP request
442 * @param httpResponse current HTTP response
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);
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();
462 AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
463 loginContext.getAuthenticationInstant(), loginContext.getAuthenticationDuration());
465 idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
467 ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
469 idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
473 * Adds an IdP session cookie to the outbound response.
475 * @param httpRequest current request
476 * @param httpResponse current response
477 * @param userSession user's session
479 protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
480 Session userSession) {
481 httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
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);
489 httpResponse.addCookie(sessionCookie);