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