Forced authentication does not reset the AuthnInstant
[java-idp.git] / src / main / java / edu / internet2 / middleware / shibboleth / idp / authn / AuthenticationEngine.java
index 27e0a64..4bed051 100644 (file)
@@ -21,9 +21,11 @@ import java.security.GeneralSecurityException;
 import java.security.MessageDigest;
 import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.Map.Entry;
@@ -40,13 +42,13 @@ import javax.servlet.http.HttpServletResponse;
 
 import org.joda.time.DateTime;
 import org.opensaml.saml2.core.AuthnContext;
+import org.opensaml.util.URLBuilder;
 import org.opensaml.util.storage.StorageService;
 import org.opensaml.ws.transport.http.HTTPTransportUtils;
 import org.opensaml.xml.util.Base64;
 import org.opensaml.xml.util.DatatypeHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.slf4j.helpers.MessageFormatter;
 
 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
@@ -126,17 +128,13 @@ public class AuthenticationEngine extends HttpServlet {
         } else {
             retainSubjectsPublicCredentials = false;
         }
-
-        handlerManager = HttpServletHelper.getProfileHandlerManager(config.getServletContext());
-        sessionManager = HttpServletHelper.getSessionManager(config.getServletContext());
-        storageService = (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(config
-                .getServletContext());
-
         context = config.getServletContext();
+        handlerManager = HttpServletHelper.getProfileHandlerManager(context);
+        sessionManager = HttpServletHelper.getSessionManager(context);
+        storageService = (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(context);
     }
 
-    /**
-     * Returns control back to the authentication engine.
+    /* Returns control back to the authentication engine.
      * 
      * @param httpRequest current HTTP request
      * @param httpResponse current HTTP response
@@ -155,7 +153,6 @@ public class AuthenticationEngine extends HttpServlet {
     /**
      * Returns control back to the profile handler that invoked the authentication engine.
      * 
-     * @param loginContext current login context
      * @param httpRequest current HTTP request
      * @param httpResponse current HTTP response
      */
@@ -167,9 +164,14 @@ public class AuthenticationEngine extends HttpServlet {
             forwardRequest("/error.jsp", httpRequest, httpResponse);
         }
 
-        HttpServletHelper.bindLoginContext(loginContext, httpRequest);
-        LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
-        forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
+        String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, loginContext.getProfileHandlerURL())
+                .buildURL();
+        LOG.debug("Redirecting user to profile handler at {}", profileUrl);
+        try {
+            httpResponse.sendRedirect(profileUrl);
+        } catch (IOException e) {
+            LOG.warn("Error sending user back to profile handler at " + profileUrl, e);
+        }
     }
 
     /**
@@ -234,7 +236,6 @@ public class AuthenticationEngine extends HttpServlet {
             }
 
             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
-            LOG.debug("Possible authentication handlers for this request: {}", possibleLoginHandlers);
 
             // Filter out possible candidate login handlers by forced and passive authentication requirements
             if (loginContext.isForceAuthRequired()) {
@@ -245,33 +246,7 @@ public class AuthenticationEngine extends HttpServlet {
                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
             }
 
-            // If the user already has a session and its usage is acceptable than use it
-            // otherwise just use the first candidate login handler
-            LOG.debug("Possible authentication handlers after filtering: {}", possibleLoginHandlers);
-            LoginHandler loginHandler;
-            if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
-                loginContext.setAttemptedAuthnMethod(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
-                loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
-            } else {
-                possibleLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
-                if (possibleLoginHandlers.isEmpty()) {
-                    LOG.info("No authentication mechanism available for use with relying party '{}'", loginContext
-                            .getRelyingPartyId());
-                    throw new AuthenticationException();
-                }
-
-                if (loginContext.getDefaultAuthenticationMethod() != null
-                        && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
-                    loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
-                    loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
-                } else {
-                    Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
-                    loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
-                    loginHandler = chosenLoginHandler.getValue();
-                }
-            }
-
-            LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
+            LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
             loginContext.setAuthenticationAttempted();
             loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
 
@@ -297,50 +272,31 @@ public class AuthenticationEngine extends HttpServlet {
      */
     protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
             throws AuthenticationException {
-        Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(handlerManager
-                .getLoginHandlers());
-        LOG.debug("Filtering configured login handlers by requested athentication methods.");
-        LOG.debug("Configured LoginHandlers: {}", supportedLoginHandlers);
-        LOG.debug("Requested authentication methods: {}", loginContext.getRequestedAuthenticationMethods());
-
-        // If no preferences Authn method preference is given, then we're free to use any
-        if (loginContext.getRequestedAuthenticationMethods().isEmpty()) {
-            LOG.trace("No preference given for authentication methods");
-            return supportedLoginHandlers;
-        }
-
-        // If the previous session handler is configured, the user has an existing session, and the SP requested
-        // that a certain set of authentication methods be used then we need to check to see if the user has
-        // authenticated with one or more of those methods, if not we can't use the previous session handler
-        if (supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX) && idpSession != null
-                && loginContext.getRequestedAuthenticationMethods() != null) {
-            boolean retainPreviousSession = false;
-
-            Map<String, AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods();
-            for (String currentAuthnMethod : currentAuthnMethods.keySet()) {
-                if (loginContext.getRequestedAuthenticationMethods().contains(currentAuthnMethod)) {
-                    retainPreviousSession = true;
-                    break;
+        Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(
+                handlerManager.getLoginHandlers());
+        LOG.debug("Filtering configured LoginHandlers: {}", supportedLoginHandlers);
+
+        // First, if the service provider requested a particular authentication method, filter out everything but
+        List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
+        if (requestedMethods != null && !requestedMethods.isEmpty()) {
+            LOG.debug("Filtering possible login handlers by requested authentication methods: {}", requestedMethods);
+            Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet()
+                    .iterator();
+            Entry<String, LoginHandler> supportedLoginHandlerEntry;
+            while (supportedLoginHandlerItr.hasNext()) {
+                supportedLoginHandlerEntry = supportedLoginHandlerItr.next();
+                if (!supportedLoginHandlerEntry.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
+                        && !requestedMethods.contains(supportedLoginHandlerEntry.getKey())) {
+                    LOG.debug(
+                            "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
+                            supportedLoginHandlerEntry.getKey());
+                    supportedLoginHandlerItr.remove();
                 }
             }
-
-            if (!retainPreviousSession) {
-                supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
-            }
         }
 
-        // Otherwise we need to filter all the mechanism supported by the IdP so that only the request types are left
-        // Previous session handler is a special case, we always to keep that around if it's configured
-        Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet().iterator();
-        Entry<String, LoginHandler> supportedLoginHandler;
-        while (supportedLoginHandlerItr.hasNext()) {
-            supportedLoginHandler = supportedLoginHandlerItr.next();
-            if (!supportedLoginHandler.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
-                    && !loginContext.getRequestedAuthenticationMethods().contains(supportedLoginHandler.getKey())) {
-                supportedLoginHandlerItr.remove();
-                continue;
-            }
-        }
+        // Next, determine, if present, if the previous session handler can be used
+        filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
 
         if (supportedLoginHandlers.isEmpty()) {
             LOG.warn("No authentication method, requested by the service provider, is supported");
@@ -352,6 +308,61 @@ public class AuthenticationEngine extends HttpServlet {
     }
 
     /**
+     * Filters out the previous session login handler if there is no existing IdP session, no active authentication
+     * methods, or if at least one of the active authentication methods do not match the requested authentication
+     * methods.
+     * 
+     * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
+     * @param idpSession current IdP session, may be null if no session currently exists
+     * @param loginContext current login context, never null
+     */
+    protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
+            Session idpSession, LoginContext loginContext) {
+        if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
+            return;
+        }
+
+        if (idpSession == null) {
+            LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
+            supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
+            return;
+        }
+        Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
+                .values();
+
+        Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
+        while (methodItr.hasNext()) {
+            AuthenticationMethodInformation info = methodItr.next();
+            if (info.isExpired()) {
+                methodItr.remove();
+            }
+        }
+        if (currentAuthnMethods.isEmpty()) {
+            LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
+            supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
+            return;
+        }
+
+        List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
+        if (requestedMethods != null && !requestedMethods.isEmpty()) {
+            boolean retainPreviousSession = false;
+            for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
+                if (loginContext.getRequestedAuthenticationMethods().contains(
+                        currentAuthnMethod.getAuthenticationMethod())) {
+                    retainPreviousSession = true;
+                    break;
+                }
+            }
+
+            if (!retainPreviousSession) {
+                LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
+                supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
+                return;
+            }
+        }
+    }
+
+    /**
      * Filters out any login handler based on the requirement for forced authentication.
      * 
      * During forced authentication any handler that has not previously been used to authenticate the user or any
@@ -379,6 +390,8 @@ public class AuthenticationEngine extends HttpServlet {
             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
+                    LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
+                            .getClass().getName());
                     loginHandlers.remove(handlerSupportedMethods);
                 }
             }
@@ -430,6 +443,56 @@ public class AuthenticationEngine extends HttpServlet {
     }
 
     /**
+     * Selects a login handler from a list of possible login handlers that could be used for the request.
+     * 
+     * @param possibleLoginHandlers list of possible login handlers that could be used for the request
+     * @param loginContext current login context
+     * @param idpSession current IdP session, if one exists
+     * 
+     * @return the login handler to use for this request
+     * 
+     * @throws AuthenticationException thrown if no handler can be used for this request
+     */
+    protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
+            LoginContext loginContext, Session idpSession) throws AuthenticationException {
+        LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
+        LoginHandler loginHandler;
+        if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
+            LOG.debug("Authenticating user with previous session LoginHandler");
+            loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
+
+            for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
+                if (authnMethod.isExpired()) {
+                    continue;
+                }
+
+                if (loginContext.getRequestedAuthenticationMethods().isEmpty()
+                        || loginContext.getRequestedAuthenticationMethods().contains(
+                                authnMethod.getAuthenticationMethod())) {
+                    LOG.debug("Basing previous session authentication on active authentication method {}",
+                            authnMethod.getAuthenticationMethod());
+                    loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
+                    loginContext.setAuthenticationMethodInformation(authnMethod);
+                    return loginHandler;
+                }
+            }
+        }
+
+        if (loginContext.getDefaultAuthenticationMethod() != null
+                && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
+            loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
+            loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
+        } else {
+            Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
+            loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
+            loginHandler = chosenLoginHandler.getValue();
+        }
+
+        LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
+        return loginHandler;
+    }
+
+    /**
      * Completes the authentication process.
      * 
      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
@@ -454,30 +517,38 @@ public class AuthenticationEngine extends HttpServlet {
             if (actualAuthnMethod != null) {
                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
-                    String msg = MessageFormatter.format(
-                                    "Relying patry required an authentication method of '{}' but the login handler performed '{}'",
-                                    loginContext.getRequestedAuthenticationMethods(), actualAuthnMethod);
+                    String msg = "Relying patry required an authentication method of "
+                            + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
+                            + actualAuthnMethod;
                     LOG.error(msg);
                     throw new AuthenticationException(msg);
                 }
             } else {
                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
             }
-
+            
             // Check to make sure the login handler did the right thing
             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
 
+            // Check for an overridden authn instant.
+            DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
+
             // Get the Subject from the request. If force authentication was required then make sure the
             // Subject identifies the same user that authenticated before
             Subject subject = getLoginHandlerSubject(httpRequest);
             if (loginContext.isForceAuthRequired()) {
                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
+                
+                // Reset the authn instant.
+                if (actualAuthnInstant == null) {
+                    actualAuthnInstant = new DateTime();
+                }
             }
 
             loginContext.setPrincipalAuthenticated(true);
-            updateUserSession(loginContext, subject, actualAuthnMethod, httpRequest, httpResponse);
-            LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(), loginContext
-                    .getAuthenticationMethod());
+            updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
+            LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
+                    loginContext.getAuthenticationMethod());
         } catch (AuthenticationException e) {
             LOG.error("Authentication failed with the error:", e);
             loginContext.setPrincipalAuthenticated(false);
@@ -503,11 +574,16 @@ public class AuthenticationEngine extends HttpServlet {
             String authenticationMethod) throws AuthenticationException {
         LOG.debug("Validating authentication was performed successfully");
 
+        if (authenticationMethod == null) {
+            LOG.error("No authentication method reported by login handler.");
+            throw new AuthenticationException("No authentication method reported by login handler.");
+        }
+
         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
         if (errorMessage != null) {
-            LOG.error("Error returned from login handler for authentication method {}:\n{}", loginContext
-                    .getAttemptedAuthnMethod(), errorMessage);
+            LOG.error("Error returned from login handler for authentication method {}:\n{}",
+                    loginContext.getAttemptedAuthnMethod(), errorMessage);
             throw new AuthenticationException(errorMessage);
         }
 
@@ -593,11 +669,13 @@ public class AuthenticationEngine extends HttpServlet {
      * @param loginContext current login context
      * @param authenticationSubject subject created from the authentication method
      * @param authenticationMethod the method used to authenticate the subject
+     * @param authenticationInstant the time of authentication
      * @param httpRequest current HTTP request
      * @param httpResponse current HTTP response
      */
     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
-            String authenticationMethod, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+            String authenticationMethod, DateTime authenticationInstant,
+            HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
 
@@ -613,12 +691,22 @@ public class AuthenticationEngine extends HttpServlet {
         // login handler subject
         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
 
-        LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
-                authenticationPrincipal.getName());
-        LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
-        AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession
-                .getSubject(), authenticationPrincipal, authenticationMethod, new DateTime(), loginHandler
-                .getAuthenticationDuration());
+        // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
+        // if not record the new information
+        AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
+                authenticationMethod);
+        if (authnMethodInfo == null || authenticationInstant != null) {
+            LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
+                    authenticationPrincipal.getName());
+            LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
+            authnMethodInfo = new AuthenticationMethodInformationImpl(
+                    idpSession.getSubject(),
+                    authenticationPrincipal,
+                    authenticationMethod,
+                    (authenticationInstant != null ? authenticationInstant : new DateTime()),
+                    loginHandler.getAuthenticationDuration()
+                    );
+        }
 
         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
@@ -705,18 +793,16 @@ public class AuthenticationEngine extends HttpServlet {
         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
         cookieValue.append(signature);
-        Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
 
-        String contextPath = httpRequest.getContextPath();
-        if (DatatypeHelper.isEmpty(contextPath)) {
-            sessionCookie.setPath("/");
-        } else {
-            sessionCookie.setPath(contextPath);
-        }
+        String cookieDomain = HttpServletHelper.getCookieDomain(context);
 
+        Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
+        sessionCookie.setVersion(1);
+        if (cookieDomain != null) {
+            sessionCookie.setDomain(cookieDomain);
+        }
+        sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
         sessionCookie.setSecure(httpRequest.isSecure());
-        sessionCookie.setMaxAge(-1);
-
         httpResponse.addCookie(sessionCookie);
     }
 }
\ No newline at end of file