View Javadoc

    1   /*
    2    * Copyright 2006 University Corporation for Advanced Internet Development, Inc.
    3    *
    4    * Licensed under the Apache License, Version 2.0 (the "License");
    5    * you may not use this file except in compliance with the License.
    6    * You may obtain a copy of the License at
    7    *
    8    * http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16
    17  package edu.internet2.middleware.shibboleth.idp.authn;
    18
    19  import java.io.IOException;
    20  import java.security.GeneralSecurityException;
    21  import java.security.MessageDigest;
    22  import java.security.Principal;
    23  import java.util.ArrayList;
    24  import java.util.Collection;
    25  import java.util.HashMap;
    26  import java.util.HashSet;
    27  import java.util.Iterator;
    28  import java.util.List;
    29  import java.util.Map;
    30  import java.util.Map.Entry;
    31  import java.util.Set;
    32
    33  import javax.security.auth.Subject;
    34  import javax.servlet.RequestDispatcher;
    35  import javax.servlet.ServletConfig;
    36  import javax.servlet.ServletContext;
    37  import javax.servlet.ServletException;
    38  import javax.servlet.http.Cookie;
    39  import javax.servlet.http.HttpServlet;
    40  import javax.servlet.http.HttpServletRequest;
    41  import javax.servlet.http.HttpServletResponse;
    42
    43  import org.joda.time.DateTime;
    44  import org.opensaml.saml2.core.AuthnContext;
    45  import org.opensaml.util.storage.StorageService;
    46  import org.opensaml.ws.transport.http.HTTPTransportUtils;
    47  import org.opensaml.xml.util.Base64;
    48  import org.opensaml.xml.util.DatatypeHelper;
    49  import org.slf4j.Logger;
    50  import org.slf4j.LoggerFactory;
    51
    52  import edu.internet2.middleware.shibboleth.common.session.SessionManager;
    53  import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
    54  import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
    55  import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
    56  import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
    57  import edu.internet2.middleware.shibboleth.idp.session.Session;
    58  import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
    59  import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
    60  import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
    61
    62  /** Manager responsible for handling authentication requests. */
    63  public class AuthenticationEngine extends HttpServlet {
    64
    65      /**
    66       * Name of the Servlet config init parameter that indicates whether the public credentials of a {@link Subject} are
    67       * retained after authentication.
    68       */
    69      public static final String RETAIN_PUBLIC_CREDENTIALS = "retainSubjectsPublicCredentials";
    70
    71      /**
    72       * Name of the Servlet config init parameter that indicates whether the private credentials of a {@link Subject} are
    73       * retained after authentication.
    74       */
    75      public static final String RETAIN_PRIVATE_CREDENTIALS = "retainSubjectsPrivateCredentials";
    76
    77      /** Name of the Servlet config init parameter that holds the partition name for login contexts. */
    78      public static final String LOGIN_CONTEXT_PARTITION_NAME_INIT_PARAM_NAME = "loginContextPartitionName";
    79
    80      /** Name of the Servlet config init parameter that holds lifetime of a login context in the storage service. */
    81      public static final String LOGIN_CONTEXT_LIFETIME_INIT_PARAM_NAME = "loginContextEntryLifetime";
    82
    83      /** Name of the IdP Cookie containing the IdP session ID. */
    84      public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
    85
    86      /** Name of the key under which to bind the storage service key for a login context. */
    87      public static final String LOGIN_CONTEXT_KEY_NAME = "_idp_authn_lc_key";
    88
    89      /** Serial version UID. */
    90      private static final long serialVersionUID = -8479060989001890156L;
    91
    92      /** Class logger. */
    93      private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
    94
    95      // TODO remove once HttpServletHelper does redirects
    96      private static ServletContext context;
    97
    98      /** Storage service used to store {@link LoginContext}s while authentication is in progress. */
    99      private static StorageService<String, LoginContextEntry> storageService;
    100
    101     /** Whether the public credentials of a {@link Subject} are retained after authentication. */
    102     private boolean retainSubjectsPublicCredentials;
    103
    104     /** Whether the private credentials of a {@link Subject} are retained after authentication. */
    105     private boolean retainSubjectsPrivateCredentials;
    106
    107     /** Profile handler manager. */
    108     private IdPProfileHandlerManager handlerManager;
    109
    110     /** Session manager. */
    111     private SessionManager<Session> sessionManager;
    112
    113     /** {@inheritDoc} */
    114     public void init(ServletConfig config) throws ServletException {
    115         super.init(config);
    116
    117         String retain = DatatypeHelper.safeTrimOrNullString(config.getInitParameter(RETAIN_PRIVATE_CREDENTIALS));
    118         if (retain != null) {
    119             retainSubjectsPrivateCredentials = Boolean.parseBoolean(retain);
    120         } else {
    121             retainSubjectsPrivateCredentials = false;
    122         }
    123
    124         retain = DatatypeHelper.safeTrimOrNullString(config.getInitParameter(RETAIN_PUBLIC_CREDENTIALS));
    125         if (retain != null) {
    126             retainSubjectsPublicCredentials = Boolean.parseBoolean(retain);
    127         } else {
    128             retainSubjectsPublicCredentials = false;
    129         }
    130         context = config.getServletContext();
    131         handlerManager = HttpServletHelper.getProfileHandlerManager(context);
    132         sessionManager = HttpServletHelper.getSessionManager(context);
    133         storageService = (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(context);
    134     }
    135
    136     /**
    137      * Returns control back to the authentication engine.
    138      *
    139      * @param httpRequest current HTTP request
    140      * @param httpResponse current HTTP response
    141      */
    142     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
    143         LOG.debug("Returning control to authentication engine");
    144         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
    145         if (loginContext == null) {
    146             LOG.warn("No login context available, unable to return to authentication engine");
    147             forwardRequest("/error.jsp", httpRequest, httpResponse);
    148         } else {
    149             forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
    150         }
    151     }
    152
    153     /**
    154      * Returns control back to the profile handler that invoked the authentication engine.
    155      *
    156      * @param httpRequest current HTTP request
    157      * @param httpResponse current HTTP response
    158      */
    159     public static void returnToProfileHandler(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
    160         LOG.debug("Returning control to profile handler");
    161         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
    162         if (loginContext == null) {
    163             LOG.warn("No login context available, unable to return to profile handler");
    164             forwardRequest("/error.jsp", httpRequest, httpResponse);
    165         }
    166
    167         String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, loginContext.getProfileHandlerURL())
    168                 .buildURL();
    169         LOG.debug("Redirecting user to profile handler at {}", profileUrl);
    170         try {
    171             httpResponse.sendRedirect(profileUrl);
    172         } catch (IOException e) {
    173             LOG.warn("Error sending user back to profile handler at " + profileUrl, e);
    174         }
    175     }
    176
    177     /**
    178      * Forwards a request to the given path.
    179      *
    180      * @param forwardPath path to forward the request to
    181      * @param httpRequest current HTTP request
    182      * @param httpResponse current HTTP response
    183      */
    184     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
    185             HttpServletResponse httpResponse) {
    186         try {
    187             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
    188             dispatcher.forward(httpRequest, httpResponse);
    189             return;
    190         } catch (IOException e) {
    191             LOG.error("Unable to return control back to authentication engine", e);
    192         } catch (ServletException e) {
    193             LOG.error("Unable to return control back to authentication engine", e);
    194         }
    195     }
    196
    197     /** {@inheritDoc} */
    198     @SuppressWarnings("unchecked")
    199     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
    200             IOException {
    201         LOG.debug("Processing incoming request");
    202
    203         if (httpResponse.isCommitted()) {
    204             LOG.error("HTTP Response already committed");
    205         }
    206
    207         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, getServletContext(), httpRequest);
    208         if (loginContext == null) {
    209             LOG.error("Incoming request does not have attached login context");
    210             throw new ServletException("Incoming request does not have attached login context");
    211         }
    212
    213         if (!loginContext.getAuthenticationAttempted()) {
    214             startUserAuthentication(loginContext, httpRequest, httpResponse);
    215         } else {
    216             completeAuthentication(loginContext, httpRequest, httpResponse);
    217         }
    218     }
    219
    220     /**
    221      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
    222      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
    223      * depending on whether passive authentication is required.
    224      *
    225      * @param loginContext current login context
    226      * @param httpRequest current HTTP request
    227      * @param httpResponse current HTTP response
    228      */
    229     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    230             HttpServletResponse httpResponse) {
    231         LOG.debug("Beginning user authentication process.");
    232         try {
    233             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    234             if (idpSession != null) {
    235                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
    236             }
    237
    238             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
    239
    240             // Filter out possible candidate login handlers by forced and passive authentication requirements
    241             if (loginContext.isForceAuthRequired()) {
    242                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
    243             }
    244
    245             if (loginContext.isPassiveAuthRequired()) {
    246                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
    247             }
    248
    249             LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
    250             loginContext.setAuthenticationAttempted();
    251             loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
    252
    253             // Send the request to the login handler
    254             HttpServletHelper.bindLoginContext(loginContext, storageService, getServletContext(), httpRequest,
    255                     httpResponse);
    256             loginHandler.login(httpRequest, httpResponse);
    257         } catch (AuthenticationException e) {
    258             loginContext.setAuthenticationFailure(e);
    259             returnToProfileHandler(httpRequest, httpResponse);
    260         }
    261     }
    262
    263     /**
    264      * Determines which configured login handlers will support the requested authentication methods.
    265      *
    266      * @param loginContext current login context
    267      * @param idpSession current user's session, or null if they don't have one
    268      *
    269      * @return login methods that may be used to authenticate the user
    270      *
    271      * @throws AuthenticationException thrown if no login handler meets the given requirements
    272      */
    273     protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
    274             throws AuthenticationException {
    275         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(
    276                 handlerManager.getLoginHandlers());
    277         LOG.debug("Filtering configured LoginHandlers: {}", supportedLoginHandlers);
    278
    279         // First, if the service provider requested a particular authentication method, filter out everything but
    280         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
    281         if (requestedMethods != null && !requestedMethods.isEmpty()) {
    282             LOG.debug("Filtering possible login handlers by requested authentication methods: {}", requestedMethods);
    283             Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet()
    284                     .iterator();
    285             Entry<String, LoginHandler> supportedLoginHandlerEntry;
    286             while (supportedLoginHandlerItr.hasNext()) {
    287                 supportedLoginHandlerEntry = supportedLoginHandlerItr.next();
    288                 if (!supportedLoginHandlerEntry.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
    289                         && !requestedMethods.contains(supportedLoginHandlerEntry.getKey())) {
    290                     LOG.debug(
    291                             "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
    292                             supportedLoginHandlerEntry.getKey());
    293                     supportedLoginHandlerItr.remove();
    294                 }
    295             }
    296         }
    297
    298         // Next, determine, if present, if the previous session handler can be used
    299         filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
    300
    301         if (supportedLoginHandlers.isEmpty()) {
    302             LOG.warn("No authentication method, requested by the service provider, is supported");
    303             throw new AuthenticationException(
    304                     "No authentication method, requested by the service provider, is supported");
    305         }
    306
    307         return supportedLoginHandlers;
    308     }
    309
    310     /**
    311      * Filters out the previous session login handler if there is no existing IdP session, no active authentication
    312      * methods, or if at least one of the active authentication methods do not match the requested authentication
    313      * methods.
    314      *
    315      * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
    316      * @param idpSession current IdP session, may be null if no session currently exists
    317      * @param loginContext current login context, never null
    318      */
    319     protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
    320             Session idpSession, LoginContext loginContext) {
    321         if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    322             return;
    323         }
    324
    325         if (idpSession == null) {
    326             LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
    327             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    328             return;
    329         }
    330         Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
    331                 .values();
    332
    333         Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
    334         while (methodItr.hasNext()) {
    335             AuthenticationMethodInformation info = methodItr.next();
    336             if (info.isExpired()) {
    337                 methodItr.remove();
    338             }
    339         }
    340         if (currentAuthnMethods.isEmpty()) {
    341             LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
    342             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    343             return;
    344         }
    345
    346         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
    347         if (requestedMethods != null && !requestedMethods.isEmpty()) {
    348             boolean retainPreviousSession = false;
    349             for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
    350                 if (loginContext.getRequestedAuthenticationMethods().contains(
    351                         currentAuthnMethod.getAuthenticationMethod())) {
    352                     retainPreviousSession = true;
    353                     break;
    354                 }
    355             }
    356
    357             if (!retainPreviousSession) {
    358                 LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
    359                 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    360                 return;
    361             }
    362         }
    363     }
    364
    365     /**
    366      * Filters out any login handler based on the requirement for forced authentication.
    367      *
    368      * During forced authentication any handler that has not previously been used to authenticate the user or any
    369      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
    370      *
    371      * @param idpSession user's current IdP session
    372      * @param loginContext current login context
    373      * @param loginHandlers login handlers to filter
    374      *
    375      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
    376      */
    377     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
    378             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
    379         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
    380
    381         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
    382         if (idpSession != null) {
    383             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
    384         }
    385
    386         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    387
    388         LoginHandler loginHandler;
    389         for (AuthenticationMethodInformation activeMethod : activeMethods) {
    390             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
    391             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
    392                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
    393                     LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
    394                             .getClass().getName());
    395                     loginHandlers.remove(handlerSupportedMethods);
    396                 }
    397             }
    398         }
    399
    400         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
    401                 loginHandlers);
    402
    403         if (loginHandlers.isEmpty()) {
    404             LOG.info("Force authentication requested but no login handlers available to support it");
    405             throw new ForceAuthenticationException();
    406         }
    407     }
    408
    409     /**
    410      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
    411      * authentication is required.
    412      *
    413      * @param idpSession user's current IdP session
    414      * @param loginContext current login context
    415      * @param loginHandlers login handlers to filter
    416      *
    417      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
    418      */
    419     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
    420             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
    421         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
    422
    423         if (idpSession == null) {
    424             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    425         }
    426
    427         LoginHandler loginHandler;
    428         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
    429         while (authnMethodItr.hasNext()) {
    430             loginHandler = authnMethodItr.next().getValue();
    431             if (!loginHandler.supportsPassive()) {
    432                 authnMethodItr.remove();
    433             }
    434         }
    435
    436         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
    437                 loginHandlers);
    438
    439         if (loginHandlers.isEmpty()) {
    440             LOG.warn("Passive authentication required but no login handlers available to support it");
    441             throw new PassiveAuthenticationException();
    442         }
    443     }
    444
    445     /**
    446      * Selects a login handler from a list of possible login handlers that could be used for the request.
    447      *
    448      * @param possibleLoginHandlers list of possible login handlers that could be used for the request
    449      * @param loginContext current login context
    450      * @param idpSession current IdP session, if one exists
    451      *
    452      * @return the login handler to use for this request
    453      *
    454      * @throws AuthenticationException thrown if no handler can be used for this request
    455      */
    456     protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
    457             LoginContext loginContext, Session idpSession) throws AuthenticationException {
    458         LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
    459         LoginHandler loginHandler;
    460         if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    461             LOG.debug("Authenticating user with previous session LoginHandler");
    462             loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    463
    464             for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
    465                 if (authnMethod.isExpired()) {
    466                     continue;
    467                 }
    468
    469                 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
    470                         || loginContext.getRequestedAuthenticationMethods().contains(
    471                                 authnMethod.getAuthenticationMethod())) {
    472                     LOG.debug("Basing previous session authentication on active authentication method {}",
    473                             authnMethod.getAuthenticationMethod());
    474                     loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
    475                     loginContext.setAuthenticationMethodInformation(authnMethod);
    476                     return loginHandler;
    477                 }
    478             }
    479         }
    480
    481         if (loginContext.getDefaultAuthenticationMethod() != null
    482                 && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
    483             loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
    484             loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
    485         } else {
    486             Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
    487             loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
    488             loginHandler = chosenLoginHandler.getValue();
    489         }
    490
    491         LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
    492         return loginHandler;
    493     }
    494
    495     /**
    496      * Completes the authentication process.
    497      *
    498      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
    499      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
    500      * recorded and finally control is returned back to the profile handler.
    501      *
    502      * @param loginContext current login context
    503      * @param httpRequest current HTTP request
    504      * @param httpResponse current HTTP response
    505      */
    506     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    507             HttpServletResponse httpResponse) {
    508         LOG.debug("Completing user authentication process");
    509
    510         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    511
    512         try {
    513             // We allow a login handler to override the authentication method in the
    514             // event that it supports multiple methods
    515             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    516                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
    517             if (actualAuthnMethod != null) {
    518                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
    519                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
    520                     String msg = "Relying patry required an authentication method of "
    521                             + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
    522                             + actualAuthnMethod;
    523                     LOG.error(msg);
    524                     throw new AuthenticationException(msg);
    525                 }
    526             } else {
    527                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
    528             }
    529
    530             // Check to make sure the login handler did the right thing
    531             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
    532
    533             // Check for an overridden authn instant.
    534             DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
    535
    536             // Get the Subject from the request. If force authentication was required then make sure the
    537             // Subject identifies the same user that authenticated before
    538             Subject subject = getLoginHandlerSubject(httpRequest);
    539             if (loginContext.isForceAuthRequired()) {
    540                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
    541
    542                 // Reset the authn instant.
    543                 if (actualAuthnInstant == null) {
    544                     actualAuthnInstant = new DateTime();
    545                 }
    546             }
    547
    548             loginContext.setPrincipalAuthenticated(true);
    549             updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
    550             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
    551                     loginContext.getAuthenticationMethod());
    552         } catch (AuthenticationException e) {
    553             LOG.error("Authentication failed with the error:", e);
    554             loginContext.setPrincipalAuthenticated(false);
    555             loginContext.setAuthenticationFailure(e);
    556         }
    557
    558         returnToProfileHandler(httpRequest, httpResponse);
    559     }
    560
    561     /**
    562      * Validates that the authentication was successfully performed by the login handler. An authentication is
    563      * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
    564      * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
    565      * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
    566      *
    567      * @param loginContext current login context
    568      * @param httpRequest current HTTP request
    569      * @param authenticationMethod the authentication method used to authenticate the user
    570      *
    571      * @throws AuthenticationException thrown if the authentication was not successful
    572      */
    573     protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    574             String authenticationMethod) throws AuthenticationException {
    575         LOG.debug("Validating authentication was performed successfully");
    576
    577         if (authenticationMethod == null) {
    578             LOG.error("No authentication method reported by login handler.");
    579             throw new AuthenticationException("No authentication method reported by login handler.");
    580         }
    581
    582         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    583                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
    584         if (errorMessage != null) {
    585             LOG.error("Error returned from login handler for authentication method {}:\n{}",
    586                     loginContext.getAttemptedAuthnMethod(), errorMessage);
    587             throw new AuthenticationException(errorMessage);
    588         }
    589
    590         AuthenticationException authnException = (AuthenticationException) httpRequest
    591                 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
    592         if (authnException != null) {
    593             throw authnException;
    594         }
    595
    596         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    597         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    598         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    599                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    600
    601         if (subject == null && principal == null && principalName == null) {
    602             LOG.error("No user identified by login handler.");
    603             throw new AuthenticationException("No user identified by login handler.");
    604         }
    605     }
    606
    607     /**
    608      * Gets the subject from the request coming back from the login handler.
    609      *
    610      * @param httpRequest request coming back from the login handler
    611      *
    612      * @return the {@link Subject} created from the request
    613      *
    614      * @throws AuthenticationException thrown if no subject can be retrieved from the request
    615      */
    616     protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
    617         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    618         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    619         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    620                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    621
    622         if (subject == null && (principal != null || principalName != null)) {
    623             subject = new Subject();
    624             if (principal == null) {
    625                 principal = new UsernamePrincipal(principalName);
    626             }
    627             subject.getPrincipals().add(principal);
    628         }
    629
    630         return subject;
    631     }
    632
    633     /**
    634      * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
    635      * principal name that is equal to the principal name associated with the authentication method. If this is the
    636      * first time the subject has authenticated with this method than this check always passes.
    637      *
    638      * @param idpSession user's IdP session
    639      * @param authnMethod method used to authenticate the user
    640      * @param subject subject that was authenticated
    641      *
    642      * @throws AuthenticationException thrown if this check fails
    643      */
    644     protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
    645             throws AuthenticationException {
    646         if (idpSession != null) {
    647             AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
    648             if (authnMethodInfo != null) {
    649                 boolean princpalMatch = false;
    650                 for (Principal princpal : subject.getPrincipals()) {
    651                     if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
    652                         princpalMatch = true;
    653                         break;
    654                     }
    655                 }
    656
    657                 if (!princpalMatch) {
    658                     throw new ForceAuthenticationException(
    659                             "Authenticated principal does not match previously authenticated principal");
    660                 }
    661             }
    662         }
    663     }
    664
    665     /**
    666      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
    667      * created.
    668      *
    669      * @param loginContext current login context
    670      * @param authenticationSubject subject created from the authentication method
    671      * @param authenticationMethod the method used to authenticate the subject
    672      * @param authenticationInstant the time of authentication
    673      * @param httpRequest current HTTP request
    674      * @param httpResponse current HTTP response
    675      */
    676     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
    677             String authenticationMethod, DateTime authenticationInstant, HttpServletRequest httpRequest,
    678             HttpServletResponse httpResponse) {
    679         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
    680         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
    681
    682         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    683         if (idpSession == null) {
    684             LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
    685             idpSession = (Session) sessionManager.createSession();
    686             loginContext.setSessionID(idpSession.getSessionID());
    687             addSessionCookie(httpRequest, httpResponse, idpSession);
    688         }
    689
    690         // Merge the information in the current session subject with the information from the
    691         // login handler subject
    692         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
    693
    694         // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
    695         // if not record the new information
    696         AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
    697                 authenticationMethod);
    698         if (authnMethodInfo == null || authenticationInstant != null) {
    699             LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
    700                     authenticationPrincipal.getName());
    701             LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
    702             DateTime authnInstant = authenticationInstant;
    703             if (authnInstant == null) {
    704                 authnInstant = new DateTime();
    705             }
    706             authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession.getSubject(), authenticationPrincipal,
    707                     authenticationMethod, authnInstant, loginHandler.getAuthenticationDuration());
    708         }
    709
    710         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
    711         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
    712         sessionManager.indexSession(idpSession, idpSession.getPrincipalName());
    713
    714         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
    715                 authnMethodInfo);
    716         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
    717     }
    718
    719     /**
    720      * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
    721      * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
    722      * the private credentials from both subjects, if not the new subject will not contain private credentials. If
    723      * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
    724      * from both subjects, if not the new subject will not contain public credentials.
    725      *
    726      * @param subject1 first subject to merge, may be null
    727      * @param subject2 second subject to merge, may be null
    728      *
    729      * @return subject containing the merged information
    730      */
    731     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
    732         if (subject1 == null && subject2 == null) {
    733             return new Subject();
    734         }
    735
    736         if (subject1 == null) {
    737             return subject2;
    738         }
    739
    740         if (subject2 == null) {
    741             return subject1;
    742         }
    743
    744         Set<Principal> principals = new HashSet<Principal>(3);
    745         principals.addAll(subject1.getPrincipals());
    746         principals.addAll(subject2.getPrincipals());
    747
    748         Set<Object> publicCredentials = new HashSet<Object>(3);
    749         if (retainSubjectsPublicCredentials) {
    750             LOG.debug("Merging in subjects public credentials");
    751             publicCredentials.addAll(subject1.getPublicCredentials());
    752             publicCredentials.addAll(subject2.getPublicCredentials());
    753         }
    754
    755         Set<Object> privateCredentials = new HashSet<Object>(3);
    756         if (retainSubjectsPrivateCredentials) {
    757             LOG.debug("Merging in subjects private credentials");
    758             privateCredentials.addAll(subject1.getPrivateCredentials());
    759             privateCredentials.addAll(subject2.getPrivateCredentials());
    760         }
    761
    762         return new Subject(false, principals, publicCredentials, privateCredentials);
    763     }
    764
    765     /**
    766      * Adds an IdP session cookie to the outbound response.
    767      *
    768      * @param httpRequest current request
    769      * @param httpResponse current response
    770      * @param userSession user's session
    771      */
    772     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
    773             Session userSession) {
    774         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
    775
    776         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
    777         byte[] sessionId = userSession.getSessionID().getBytes();
    778
    779         String signature = null;
    780         try {
    781             MessageDigest digester = MessageDigest.getInstance("SHA");
    782             digester.update(userSession.getSessionSecret());
    783             digester.update(remoteAddress);
    784             digester.update(sessionId);
    785             signature = Base64.encodeBytes(digester.digest());
    786         } catch (GeneralSecurityException e) {
    787             LOG.error("Unable to compute signature over session cookie material", e);
    788         }
    789
    790         LOG.debug("Adding IdP session cookie to HTTP response");
    791         StringBuilder cookieValue = new StringBuilder();
    792         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
    793         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
    794         cookieValue.append(signature);
    795
    796         String cookieDomain = HttpServletHelper.getCookieDomain(context);
    797
    798         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
    799         sessionCookie.setVersion(1);
    800         if (cookieDomain != null) {
    801             sessionCookie.setDomain(cookieDomain);
    802         }
    803         sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
    804         sessionCookie.setSecure(httpRequest.isSecure());
    805         httpResponse.addCookie(sessionCookie);
    806     }
    807 }