tag attempted authentication trip
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / authn / AuthenticationEngine.java
index 4316219..2f1ff69 100644 (file)
 package edu.internet2.middleware.shibboleth.idp.authn;
 
 import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.List;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
 
+import javax.security.auth.Subject;
 import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
-import org.apache.log4j.Logger;
 import org.joda.time.DateTime;
-import org.opensaml.xml.util.Pair;
+import org.opensaml.xml.util.DatatypeHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import edu.internet2.middleware.shibboleth.common.profile.ProfileHandlerManager;
 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
+import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
+import edu.internet2.middleware.shibboleth.idp.authn.provider.PreviousSessionLoginHandler;
+import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
 import edu.internet2.middleware.shibboleth.idp.session.Session;
@@ -45,217 +53,440 @@ import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationIm
  */
 public class AuthenticationEngine extends HttpServlet {
 
+    /** Name of the IdP Cookie containing the IdP session ID. */
+    public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 8494202791991613148L;
+
     /** Class logger. */
-    private static final Logger log = Logger.getLogger(AuthenticationEngine.class);
+    private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
 
-    /**
-     * Gets the manager used to retrieve handlers for requests.
-     * 
-     * @return manager used to retrieve handlers for requests
-     */
-    public ProfileHandlerManager getProfileHandlerManager() {
-        return (ProfileHandlerManager) getServletContext().getAttribute("handlerManager");
+    /** Profile handler manager. */
+    private IdPProfileHandlerManager handlerManager;
+
+    /** Session manager. */
+    private SessionManager<Session> sessionManager;
+
+    /** {@inheritDoc} */
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+
+        String handlerManagerId = config.getInitParameter("handlerManagerId");
+        if (DatatypeHelper.isEmpty(handlerManagerId)) {
+            handlerManagerId = "shibboleth.HandlerManager";
+        }
+        handlerManager = (IdPProfileHandlerManager) getServletContext().getAttribute(handlerManagerId);
+
+        String sessionManagerId = config.getInitParameter("sessionManagedId");
+        if (DatatypeHelper.isEmpty(sessionManagerId)) {
+            sessionManagerId = "shibboleth.SessionManager";
+        }
+
+        sessionManager = (SessionManager<Session>) getServletContext().getAttribute(sessionManagerId);
     }
 
     /**
-     * Gets the session manager to be used.
+     * Returns control back to the authentication engine.
      * 
-     * @return session manager to be used
+     * @param httpRequest current http request
+     * @param httpResponse current http response
      */
-    @SuppressWarnings("unchecked")
-    public SessionManager<Session> getSessionManager() {
-        return (SessionManager<Session>) getServletContext().getAttribute("sessionManager");
+    public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+        LOG.debug("Returning control to authentication engine");
+        HttpSession httpSession = httpRequest.getSession();
+        LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
+        if (loginContext == null) {
+            LOG.error("User HttpSession did not contain a login context.  Unable to return to authentication engine");
+            forwardRequest("/idp-error.jsp", httpRequest, httpResponse);
+        } else {
+            forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
+        }
     }
 
     /**
-     * Gets the authentication handler manager used by this engine.
+     * Returns control back to the profile handler that invoked the authentication engine.
      * 
-     * @return authentication handler manager used by this engine
+     * @param loginContext current login context
+     * @param httpRequest current http request
+     * @param httpResponse current http response
      */
-    public AuthenticationHandlerManager getAuthenticationHandlerManager() {
-        return (AuthenticationHandlerManager) getServletContext().getAttribute("authenticationHandlerManager");
+    public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
+        httpRequest.getSession().removeAttribute(LoginContext.LOGIN_CONTEXT_KEY);
+        httpRequest.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
+        forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
     }
-    
 
     /**
-     * Returns control back to the authentication engine.
+     * Forwards a request to the given path.
      * 
-     * @param httpRequest current http request
-     * @param httpResponse current http response
-     * @param loginContext user login context
+     * @param forwardPath path to forward the request to
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
      */
-    public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
-        HttpSession httpSession = httpRequest.getSession();
-
-        LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
-        
+    protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
         try {
-            RequestDispatcher distpather = httpRequest.getRequestDispatcher(loginContext.getAuthenticationManagerURL());
-            distpather.forward(httpRequest, httpResponse);
+            RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
+            dispatcher.forward(httpRequest, httpResponse);
+            return;
         } catch (IOException e) {
-            log.fatal("Unable to return control back to authentication engine", e);
+            LOG.error("Unable to return control back to authentication engine", e);
         } catch (ServletException e) {
-            log.fatal("Unable to return control back to authentication engine", e);
+            LOG.error("Unable to return control back to authentication engine", e);
         }
     }
 
     /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
             IOException {
-        HttpSession httpSession = httpRequest.getSession();
+        LOG.debug("Processing incoming request");
 
-        LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
+        if (httpResponse.isCommitted()) {
+            LOG.error("HTTP Response already committed");
+        }
+
+        LoginContext loginContext = (LoginContext) httpRequest.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
         if (loginContext == null) {
-            // TODO error
+            // When the login context comes from the profile handlers its attached to the request
+            // The authn engine attaches it to the session to allow the handlers to do any number of
+            // request/response pairs without maintaining or losing the login context
+            loginContext = (LoginContext) httpRequest.getSession().getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
         }
 
-        // If authentication has been attempted, don't try it again.
-        if (loginContext.getAuthenticationAttempted()) {
-            handleNewAuthnRequest(loginContext, httpRequest, httpResponse);
+        if (loginContext == null) {
+            LOG.error("Incoming request does not have attached login context");
+            throw new ServletException("Incoming request does not have attached login context");
+        }
+
+        if (!loginContext.getAuthenticationAttempted()) {
+            startUserAuthentication(loginContext, httpRequest, httpResponse);
         } else {
-            finishAuthnRequest(loginContext, httpRequest, httpResponse);
+            completeAuthentication(loginContext, httpRequest, httpResponse);
         }
     }
 
     /**
-     * Handle a new authentication request.
-     * 
-     * @param loginContext The {@link LoginContext} for the new authentication request
-     * @param httpRequest The servlet request containing the authn request
-     * @param httpResponse The associated servlet response.
+     * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
+     * authentication method is sufficient. Also determines, when authentication is required, which handler to use
+     * depending on whether passive authentication is required.
      * 
-     * @throws IOException thrown if there is a problem reading/writting to the HTTP request/response
-     * @throws ServletException thrown if there is a problem transferring control to the authentication handler
+     * @param loginContext current login context
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
      */
-    protected void handleNewAuthnRequest(LoginContext loginContext, HttpServletRequest httpRequest,
-            HttpServletResponse httpResponse) throws ServletException, IOException {
+    protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        LOG.debug("Beginning user authentication process");
+        try {
+            Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
+            if(idpSession != null){
+                LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
+            }
+            
+            Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(loginContext);
+            LOG.debug("Possible authentication handlers for this request: {}", possibleLoginHandlers);
 
-        HttpSession httpSession = httpRequest.getSession();
-        String shibSessionId = (String) httpSession.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
-        Session shibSession = getSessionManager().getSession(shibSessionId);
+            // Filter out possible candidate login handlers by forced and passive authentication requirements
+            if (loginContext.isForceAuthRequired()) {
+                filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
+            }
 
-        AuthenticationMethodInformation authenticationMethod = getUsableExistingAuthenticationMethod(loginContext,
-                shibSession);
-        if (authenticationMethod != null) {
-            loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
-            loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
-            loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
-            loginContext.setPrincipalAuthenticated(true);
-            loginContext.setPrincipalName(shibSession.getPrincipalName());
-            finishAuthnRequest(loginContext, httpRequest, httpResponse);
-        } else {
-            Pair<String, AuthenticationHandler> handler = getAuthenticationHandlerManager().getAuthenticationHandler(
-                    loginContext);
-
-            if (handler == null) {
-                loginContext.setPassiveAuth(false);
-                loginContext
-                        .setAuthenticationFailureMessage("No installed AuthenticationHandler can satisfy the authentication request.");
-                log.error("No installed AuthenticationHandler can satisfy the authentication request.");
-                finishAuthnRequest(loginContext, httpRequest, httpResponse);
+            if (loginContext.isPassiveAuthRequired()) {
+                filterByPassiveAuthentication(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);
+            if (idpSession != null
+                    && possibleLoginHandlers.containsKey(PreviousSessionLoginHandler.PREVIOUS_SESSION_AUTHN_METHOD)) {
+                authenticateUserWithPreviousSession(loginContext, possibleLoginHandlers, httpRequest, httpResponse);
+            } else {
+                Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
+                authenticateUser(chosenLoginHandler.getKey(), chosenLoginHandler.getValue(), loginContext, httpRequest,
+                        httpResponse);
             }
+        } catch (AuthenticationException e) {
+            loginContext.setAuthenticationFailure(e);
+            returnToProfileHandler(loginContext, httpRequest, httpResponse);
+        }
+    }
 
-            loginContext.setAuthenticationAttempted();
-            loginContext.setAuthenticationDuration(handler.getSecond().getAuthenticationDuration());
-            loginContext.setAuthenticationMethod(handler.getFirst());
-            loginContext.setAuthenticationManagerURL(httpRequest.getRequestURI());
+    /**
+     * Determines which configured login handlers will support the requested authentication methods.
+     * 
+     * @param loginContext current login context
+     * 
+     * @return login methods that may be used to authenticate the user
+     * 
+     * @throws AuthenticationException thrown if no login handler meets the given requirements
+     */
+    protected Map<String, LoginHandler> determinePossibleLoginHandlers(LoginContext loginContext)
+            throws AuthenticationException {
+        Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(handlerManager
+                .getLoginHandlers());
+        LOG.trace("Supported login handlers: {}", supportedLoginHandlers);
+        LOG.trace("Requested authentication methods: {}", loginContext.getRequestedAuthenticationMethods());
+
+        Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet().iterator();
+        Entry<String, LoginHandler> supportedLoginHandler;
+        while (supportedLoginHandlerItr.hasNext()) {
+            supportedLoginHandler = supportedLoginHandlerItr.next();
+            if (!(supportedLoginHandler.getKey().equals(PreviousSessionLoginHandler.PREVIOUS_SESSION_AUTHN_METHOD))
+                    && !loginContext.getRequestedAuthenticationMethods().contains(supportedLoginHandler.getKey())) {
+                supportedLoginHandlerItr.remove();
+                continue;
+            }
+        }
 
-            httpSession.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
-            handler.getSecond().login(loginContext, httpRequest, httpResponse);
+        if (supportedLoginHandlers.isEmpty()) {
+            LOG.error("No authentication method, requested by the service provider, is supported");
+            throw new AuthenticationException(
+                    "No authentication method, requested by the service provider, is supported");
         }
+
+        return supportedLoginHandlers;
     }
 
     /**
-     * Gets the authentication method, currently active for the user, that also meets the requirements expressed by the
-     * login context. If a method is returned the user does not need to authenticate again, if null is returned then the
-     * user must be authenticated.
+     * 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
+     * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
      * 
-     * @param loginContext user login context
-     * @param shibSession user's shibboleth session
+     * @param idpSession user's current IdP session
+     * @param loginContext current login context
+     * @param loginHandlers login handlers to filter
      * 
-     * @return active authentication method that meets authentication requirements or null
+     * @throws ForceAuthenticationException thrown if no handlers remain after filtering
      */
-    protected AuthenticationMethodInformation getUsableExistingAuthenticationMethod(LoginContext loginContext,
-            Session shibSession) {
-        if (loginContext.getForceAuth() || shibSession == null) {
-            return null;
-        }
+    protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
+            Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
+        LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
 
-        List<String> preferredAuthnMethods = loginContext.getRequestedAuthenticationMethods();
+        ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
+        if (idpSession != null) {
+            activeMethods.addAll(idpSession.getAuthenticationMethods().values());
+        }
 
-        if (preferredAuthnMethods == null || preferredAuthnMethods.size() == 0) {
-            for (AuthenticationMethodInformation authnMethod : shibSession.getAuthenticationMethods().values()) {
-                if (!authnMethod.isExpired()) {
-                    return authnMethod;
-                }
-            }
-        } else {
-            for (String preferredAuthnMethod : preferredAuthnMethods) {
-                if (shibSession.getAuthenticationMethods().containsKey(preferredAuthnMethod)) {
-                    AuthenticationMethodInformation authnMethodInfo = shibSession.getAuthenticationMethods().get(
-                            preferredAuthnMethod);
-                    if (!authnMethodInfo.isExpired()) {
-                        return authnMethodInfo;
-                    }
+        LoginHandler loginHandler;
+        for (AuthenticationMethodInformation activeMethod : activeMethods) {
+            loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
+            if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
+                for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
+                    loginHandlers.remove(handlerSupportedMethods);
                 }
             }
         }
+        
+        LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
+                loginHandlers);
 
-        return null;
+        if (loginHandlers.isEmpty()) {
+            LOG.error("Force authentication required but no login handlers available to support it");
+            throw new ForceAuthenticationException();
+        }
     }
 
     /**
-     * Handle the "return leg" of an authentication request (i.e. clean up after an authentication handler has run).
+     * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
+     * authentication is required.
      * 
-     * @param loginContext The {@link LoginContext} for the new authentication request
-     * @param httpRequest The servlet request containing the authn request
-     * @param httpResponse The associated servlet response.
+     * @param loginContext current login context
+     * @param loginHandlers login handlers to filter
      * 
-     * @throws IOException thrown if there is a problem reading/writting to the HTTP request/response
-     * @throws ServletException thrown if there is a problem transferring control to the authentication profile handler
+     * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
      */
-    protected void finishAuthnRequest(LoginContext loginContext, HttpServletRequest httpRequest,
-            HttpServletResponse httpResponse) throws ServletException, IOException {
+    protected void filterByPassiveAuthentication(LoginContext loginContext, Map<String, LoginHandler> loginHandlers)
+            throws PassiveAuthenticationException {
+        LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
+
+        LoginHandler loginHandler;
+        Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
+        while (authnMethodItr.hasNext()) {
+            loginHandler = authnMethodItr.next().getValue();
+            if (!loginHandler.supportsPassive()) {
+                authnMethodItr.remove();
+            }
+        }
+        
+        LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
+                loginHandlers);
 
-        HttpSession httpSession = httpRequest.getSession();
-        String shibSessionId = (String) httpSession.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
-        Session shibSession = null;
-        AuthenticationMethodInformation authnMethodInfo = null;
-        ServiceInformation serviceInfo = null;
+        if (loginHandlers.isEmpty()) {
+            LOG.error("Passive authentication required but no login handlers available to support it");
+            throw new PassiveAuthenticationException();
+        }
+    }
 
-        if (!loginContext.getAuthenticationAttempted()) {
-            // Authentication wasn't attempted so we're using a previously established authentication method
-            shibSession = getSessionManager().getSession(shibSessionId);
-            authnMethodInfo = shibSession.getAuthenticationMethods().get(loginContext.getAuthenticationMethod());
+    /**
+     * Completes the authentication request using an existing, active, authentication method for the current user.
+     * 
+     * @param loginContext current login context
+     * @param possibleLoginHandlers login handlers that meet the peers authentication requirements
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
+     */
+    protected void authenticateUserWithPreviousSession(LoginContext loginContext,
+            Map<String, LoginHandler> possibleLoginHandlers, HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        LOG.debug("Authenticating user by way of existing session.");
+
+        Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
+        PreviousSessionLoginHandler loginHandler = (PreviousSessionLoginHandler) handlerManager.getLoginHandlers().get(
+                PreviousSessionLoginHandler.PREVIOUS_SESSION_AUTHN_METHOD);
+
+        AuthenticationMethodInformation authenticationMethod = null;
+        for (String possibleAuthnMethod : possibleLoginHandlers.keySet()) {
+            authenticationMethod = idpSession.getAuthenticationMethods().get(possibleAuthnMethod);
+            if (authenticationMethod != null) {
+                break;
+            }
+        }
+
+        if (loginHandler.reportPreviousSessionAuthnMethod()) {
+            loginContext.setAuthenticationDuration(loginHandler.getAuthenticationDuration());
+            loginContext.setAuthenticationInstant(new DateTime());
+            loginContext.setAuthenticationMethod(PreviousSessionLoginHandler.PREVIOUS_SESSION_AUTHN_METHOD);
         } else {
-            if (shibSessionId == null) {
-                InetAddress addr;
-                try {
-                    addr = InetAddress.getByName(httpRequest.getRemoteAddr());
-                } catch (UnknownHostException ex) {
-                    addr = null;
-                }
+            loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
+            loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
+            loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
+        }
+        loginContext.setPrincipalName(idpSession.getPrincipalName());
+
+        loginContext.setAuthenticationAttempted();
+        httpRequest.getSession().setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
+        loginHandler.login(httpRequest, httpResponse);
+    }
 
-                shibSession = (Session) getSessionManager().createSession(addr, loginContext.getPrincipalName());
-                httpSession.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, shibSession.getSessionID());
+    /**
+     * Authenticates the user with the given authentication method provided by the given login handler.
+     * 
+     * @param authnMethod the authentication method that will be used to authenticate the user
+     * @param loginHandler login handler that will authenticate user
+     * @param loginContext current login context
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
+     */
+    protected void authenticateUser(String authnMethod, LoginHandler loginHandler, LoginContext loginContext,
+            HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+        LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
+
+        loginContext.setAuthenticationAttempted();
+        loginContext.setAuthenticationInstant(new DateTime());
+        loginContext.setAuthenticationDuration(loginHandler.getAuthenticationDuration());
+        loginContext.setAuthenticationMethod(authnMethod);
+        loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
+        httpRequest.getSession().setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
+        loginHandler.login(httpRequest, httpResponse);
+    }
 
-                authnMethodInfo = new AuthenticationMethodInformationImpl(loginContext.getAuthenticationMethod(),
-                        new DateTime(), loginContext.getAuthenticationDuration());
-                shibSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
+    /**
+     * Completes the authentication process.
+     * 
+     * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
+     * Shibboleth session is created if needed, information indicating that the user has logged into the service is
+     * recorded and finally control is returned back to the profile handler.
+     * 
+     * @param loginContext current login context
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
+     */
+    protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        LOG.debug("Completing user authentication process");
+
+        // We check if the principal name was already set in the login context
+        // if not attempt to pull it from where login handlers are supposed to provide it
+        String principalName = DatatypeHelper.safeTrimOrNullString(loginContext.getPrincipalName());
+        if (principalName == null) {
+            principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
+                    .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
+            if (principalName != null) {
+                loginContext.setPrincipalName(principalName);
+            } else {
+                loginContext.setPrincipalAuthenticated(false);
+                loginContext.setAuthenticationFailure(new AuthenticationException(
+                        "No principal name returned from authentication handler."));
+                LOG.error("No principal name returned from authentication method: "
+                        + loginContext.getAuthenticationMethod());
+                returnToProfileHandler(loginContext, httpRequest, httpResponse);
+                return;
             }
         }
+        loginContext.setPrincipalAuthenticated(true);
+
+        // We allow a login handler to override the authentication method in the event that it supports multiple methods
+        String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
+                .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
+        if (actualAuthnMethod != null) {
+            loginContext.setAuthenticationMethod(actualAuthnMethod);
+        }
 
-        loginContext.setSessionID(shibSession.getSessionID());
-        shibSession.setLastActivityInstant(new DateTime());
+        LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(), loginContext
+                .getAuthenticationMethod());
+        updateUserSession(loginContext, httpRequest, httpResponse);
+        returnToProfileHandler(loginContext, httpRequest, httpResponse);
+    }
 
-        serviceInfo = shibSession.getServicesInformation().get(loginContext.getRelyingPartyId());
-        if (serviceInfo == null) {
-            serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(), authnMethodInfo);
+    /**
+     * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
+     * created.
+     * 
+     * @param loginContext current login context
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
+     */
+    protected void updateUserSession(LoginContext loginContext, HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
+        if (idpSession == null) {
+            LOG.debug("Creating shibboleth session for principal {}", loginContext.getPrincipalName());
+            idpSession = (Session) sessionManager.createSession(loginContext.getPrincipalName());
+            loginContext.setSessionID(idpSession.getSessionID());
+            addSessionCookie(httpRequest, httpResponse, idpSession);
         }
 
-        RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(loginContext.getProfileHandlerURL());
-        dispatcher.forward(httpRequest, httpResponse);
+        LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
+                loginContext.getPrincipalName());
+        Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
+        String authnMethod = (String) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY);
+        if (DatatypeHelper.isEmpty(authnMethod)) {
+            authnMethod = loginContext.getAuthenticationMethod();
+        }
+
+        AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
+                loginContext.getAuthenticationInstant(), loginContext.getAuthenticationDuration());
+
+        idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
+
+        ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
+                authnMethodInfo);
+        idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
     }
 
-    // TODO logout support
+    /**
+     * Adds an IdP session cookie to the outbound response.
+     * 
+     * @param httpRequest current request
+     * @param httpResponse current response
+     * @param userSession user's session
+     */
+    protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
+            Session userSession) {
+        httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
+
+        LOG.debug("Adding IdP session cookie to HTTP response");
+        Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, userSession.getSessionID());
+        sessionCookie.setPath(httpRequest.getContextPath());
+        sessionCookie.setSecure(false);
+        sessionCookie.setMaxAge(-1);
+
+        httpResponse.addCookie(sessionCookie);
+    }
 }
\ No newline at end of file