View Javadoc

    1   /*
    2    * Licensed to the University Corporation for Advanced Internet Development,
    3    * Inc. (UCAID) under one or more contributor license agreements.  See the
    4    * NOTICE file distributed with this work for additional information regarding
    5    * copyright ownership. The UCAID licenses this file to You under the Apache
    6    * License, Version 2.0 (the "License"); you may not use this file except in
    7    * compliance with the License.  You may obtain a copy of the License at
    8    *
    9    *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package edu.internet2.middleware.shibboleth.idp.authn;
    19
    20  import java.io.IOException;
    21  import java.security.GeneralSecurityException;
    22  import java.security.MessageDigest;
    23  import java.security.Principal;
    24  import java.util.ArrayList;
    25  import java.util.Collection;
    26  import java.util.HashMap;
    27  import java.util.HashSet;
    28  import java.util.Iterator;
    29  import java.util.List;
    30  import java.util.Map;
    31  import java.util.Map.Entry;
    32  import java.util.Set;
    33
    34  import javax.security.auth.Subject;
    35  import javax.servlet.RequestDispatcher;
    36  import javax.servlet.ServletConfig;
    37  import javax.servlet.ServletContext;
    38  import javax.servlet.ServletException;
    39  import javax.servlet.http.Cookie;
    40  import javax.servlet.http.HttpServlet;
    41  import javax.servlet.http.HttpServletRequest;
    42  import javax.servlet.http.HttpServletResponse;
    43
    44  import org.joda.time.DateTime;
    45  import org.opensaml.saml2.core.AuthnContext;
    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         String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, loginContext.getProfileHandlerURL())
    169                 .buildURL();
    170         LOG.debug("Redirecting user to profile handler at {}", profileUrl);
    171         try {
    172             httpResponse.sendRedirect(profileUrl);
    173         } catch (IOException e) {
    174             LOG.warn("Error sending user back to profile handler at " + profileUrl, e);
    175         }
    176     }
    177
    178     /**
    179      * Forwards a request to the given path.
    180      *
    181      * @param forwardPath path to forward the request to
    182      * @param httpRequest current HTTP request
    183      * @param httpResponse current HTTP response
    184      */
    185     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
    186             HttpServletResponse httpResponse) {
    187         try {
    188             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
    189             dispatcher.forward(httpRequest, httpResponse);
    190             return;
    191         } catch (IOException e) {
    192             LOG.error("Unable to return control back to authentication engine", e);
    193         } catch (ServletException e) {
    194             LOG.error("Unable to return control back to authentication engine", e);
    195         }
    196     }
    197
    198     /** {@inheritDoc} */
    199     @SuppressWarnings("unchecked")
    200     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
    201             IOException {
    202         LOG.debug("Processing incoming request");
    203
    204         if (httpResponse.isCommitted()) {
    205             LOG.error("HTTP Response already committed");
    206         }
    207
    208         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, getServletContext(), httpRequest);
    209         if (loginContext == null) {
    210             LOG.warn("No login context available, unable to proceed with authentication");
    211             forwardRequest("/error.jsp", httpRequest, httpResponse);
    212             return;
    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>(
    278                 handlerManager.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.debug(
    293                             "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
    294                             supportedLoginHandlerEntry.getKey());
    295                     supportedLoginHandlerItr.remove();
    296                 }
    297             }
    298         }
    299
    300         // Next, determine, if present, if the previous session handler can be used
    301         filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
    302
    303         if (supportedLoginHandlers.isEmpty()) {
    304             LOG.warn("No authentication method, requested by the service provider, is supported");
    305             throw new AuthenticationException(
    306                     "No authentication method, requested by the service provider, is supported");
    307         }
    308
    309         return supportedLoginHandlers;
    310     }
    311
    312     /**
    313      * Filters out the previous session login handler if there is no existing IdP session, no active authentication
    314      * methods, or if at least one of the active authentication methods do not match the requested authentication
    315      * methods.
    316      *
    317      * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
    318      * @param idpSession current IdP session, may be null if no session currently exists
    319      * @param loginContext current login context, never null
    320      */
    321     protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
    322             Session idpSession, LoginContext loginContext) {
    323         if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    324             return;
    325         }
    326
    327         if (idpSession == null) {
    328             LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
    329             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    330             return;
    331         }
    332         Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
    333                 .values();
    334
    335         Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
    336         while (methodItr.hasNext()) {
    337             AuthenticationMethodInformation info = methodItr.next();
    338             if (info.isExpired()) {
    339                 methodItr.remove();
    340             }
    341         }
    342         if (currentAuthnMethods.isEmpty()) {
    343             LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
    344             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    345             return;
    346         }
    347
    348         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
    349         if (requestedMethods != null && !requestedMethods.isEmpty()) {
    350             boolean retainPreviousSession = false;
    351             for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
    352                 if (loginContext.getRequestedAuthenticationMethods().contains(
    353                         currentAuthnMethod.getAuthenticationMethod())) {
    354                     retainPreviousSession = true;
    355                     break;
    356                 }
    357             }
    358
    359             if (!retainPreviousSession) {
    360                 LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
    361                 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    362                 return;
    363             }
    364         }
    365     }
    366
    367     /**
    368      * Filters out any login handler based on the requirement for forced authentication.
    369      *
    370      * During forced authentication any handler that has not previously been used to authenticate the user or any
    371      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
    372      *
    373      * @param idpSession user's current IdP session
    374      * @param loginContext current login context
    375      * @param loginHandlers login handlers to filter
    376      *
    377      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
    378      */
    379     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
    380             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
    381         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
    382
    383         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
    384         if (idpSession != null) {
    385             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
    386         }
    387
    388         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    389
    390         LoginHandler loginHandler;
    391         for (AuthenticationMethodInformation activeMethod : activeMethods) {
    392             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
    393             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
    394                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
    395                     LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
    396                             .getClass().getName());
    397                     loginHandlers.remove(handlerSupportedMethods);
    398                 }
    399             }
    400         }
    401
    402         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
    403                 loginHandlers);
    404
    405         if (loginHandlers.isEmpty()) {
    406             LOG.info("Force authentication requested but no login handlers available to support it");
    407             throw new ForceAuthenticationException();
    408         }
    409     }
    410
    411     /**
    412      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
    413      * authentication is required.
    414      *
    415      * @param idpSession user's current IdP session
    416      * @param loginContext current login context
    417      * @param loginHandlers login handlers to filter
    418      *
    419      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
    420      */
    421     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
    422             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
    423         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
    424
    425         if (idpSession == null) {
    426             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    427         }
    428
    429         LoginHandler loginHandler;
    430         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
    431         while (authnMethodItr.hasNext()) {
    432             loginHandler = authnMethodItr.next().getValue();
    433             if (!loginHandler.supportsPassive()) {
    434                 authnMethodItr.remove();
    435             }
    436         }
    437
    438         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
    439                 loginHandlers);
    440
    441         if (loginHandlers.isEmpty()) {
    442             LOG.warn("Passive authentication required but no login handlers available to support it");
    443             throw new PassiveAuthenticationException();
    444         }
    445     }
    446
    447     /**
    448      * Selects a login handler from a list of possible login handlers that could be used for the request.
    449      *
    450      * @param possibleLoginHandlers list of possible login handlers that could be used for the request
    451      * @param loginContext current login context
    452      * @param idpSession current IdP session, if one exists
    453      *
    454      * @return the login handler to use for this request
    455      *
    456      * @throws AuthenticationException thrown if no handler can be used for this request
    457      */
    458     protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
    459             LoginContext loginContext, Session idpSession) throws AuthenticationException {
    460         LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
    461         LoginHandler loginHandler;
    462         if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    463             LOG.debug("Authenticating user with previous session LoginHandler");
    464             loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    465
    466             for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
    467                 if (authnMethod.isExpired()) {
    468                     continue;
    469                 }
    470
    471                 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
    472                         || loginContext.getRequestedAuthenticationMethods().contains(
    473                                 authnMethod.getAuthenticationMethod())) {
    474                     LOG.debug("Basing previous session authentication on active authentication method {}",
    475                             authnMethod.getAuthenticationMethod());
    476                     loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
    477                     loginContext.setAuthenticationMethodInformation(authnMethod);
    478                     return loginHandler;
    479                 }
    480             }
    481         }
    482
    483         if (loginContext.getDefaultAuthenticationMethod() != null
    484                 && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
    485             loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
    486             loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
    487         } else {
    488             Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
    489             loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
    490             loginHandler = chosenLoginHandler.getValue();
    491         }
    492
    493         LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
    494         return loginHandler;
    495     }
    496
    497     /**
    498      * Completes the authentication process.
    499      *
    500      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
    501      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
    502      * recorded and finally control is returned back to the profile handler.
    503      *
    504      * @param loginContext current login context
    505      * @param httpRequest current HTTP request
    506      * @param httpResponse current HTTP response
    507      */
    508     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    509             HttpServletResponse httpResponse) {
    510         LOG.debug("Completing user authentication process");
    511
    512         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    513
    514         try {
    515             // We allow a login handler to override the authentication method in the
    516             // event that it supports multiple methods
    517             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    518                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
    519             if (actualAuthnMethod != null) {
    520                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
    521                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
    522                     String msg = "Relying patry required an authentication method of "
    523                             + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
    524                             + actualAuthnMethod;
    525                     LOG.error(msg);
    526                     throw new AuthenticationException(msg);
    527                 }
    528             } else {
    529                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
    530             }
    531
    532             // Check to make sure the login handler did the right thing
    533             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
    534
    535             // Check for an overridden authn instant.
    536             DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
    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                 // Reset the authn instant.
    545                 if (actualAuthnInstant == null) {
    546                     actualAuthnInstant = new DateTime();
    547                 }
    548             }
    549
    550             loginContext.setPrincipalAuthenticated(true);
    551             updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
    552             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
    553                     loginContext.getAuthenticationMethod());
    554         } catch (AuthenticationException e) {
    555             LOG.error("Authentication failed with the error:", e);
    556             loginContext.setPrincipalAuthenticated(false);
    557             loginContext.setAuthenticationFailure(e);
    558         }
    559
    560         returnToProfileHandler(httpRequest, httpResponse);
    561     }
    562
    563     /**
    564      * Validates that the authentication was successfully performed by the login handler. An authentication is
    565      * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
    566      * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
    567      * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
    568      *
    569      * @param loginContext current login context
    570      * @param httpRequest current HTTP request
    571      * @param authenticationMethod the authentication method used to authenticate the user
    572      *
    573      * @throws AuthenticationException thrown if the authentication was not successful
    574      */
    575     protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    576             String authenticationMethod) throws AuthenticationException {
    577         LOG.debug("Validating authentication was performed successfully");
    578
    579         if (authenticationMethod == null) {
    580             LOG.error("No authentication method reported by login handler.");
    581             throw new AuthenticationException("No authentication method reported by login handler.");
    582         }
    583
    584         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    585                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
    586         if (errorMessage != null) {
    587             LOG.error("Error returned from login handler for authentication method {}:\n{}",
    588                     loginContext.getAttemptedAuthnMethod(), errorMessage);
    589             throw new AuthenticationException(errorMessage);
    590         }
    591
    592         AuthenticationException authnException = (AuthenticationException) httpRequest
    593                 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
    594         if (authnException != null) {
    595             throw authnException;
    596         }
    597
    598         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    599         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    600         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    601                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    602
    603         if (subject == null && principal == null && principalName == null) {
    604             LOG.error("No user identified by login handler.");
    605             throw new AuthenticationException("No user identified by login handler.");
    606         }
    607     }
    608
    609     /**
    610      * Gets the subject from the request coming back from the login handler.
    611      *
    612      * @param httpRequest request coming back from the login handler
    613      *
    614      * @return the {@link Subject} created from the request
    615      *
    616      * @throws AuthenticationException thrown if no subject can be retrieved from the request
    617      */
    618     protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
    619         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    620         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    621         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    622                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    623
    624         if (subject == null && (principal != null || principalName != null)) {
    625             subject = new Subject();
    626             if (principal == null) {
    627                 principal = new UsernamePrincipal(principalName);
    628             }
    629             subject.getPrincipals().add(principal);
    630         }
    631
    632         return subject;
    633     }
    634
    635     /**
    636      * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
    637      * principal name that is equal to the principal name associated with the authentication method. If this is the
    638      * first time the subject has authenticated with this method than this check always passes.
    639      *
    640      * @param idpSession user's IdP session
    641      * @param authnMethod method used to authenticate the user
    642      * @param subject subject that was authenticated
    643      *
    644      * @throws AuthenticationException thrown if this check fails
    645      */
    646     protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
    647             throws AuthenticationException {
    648         if (idpSession != null) {
    649             AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
    650             if (authnMethodInfo != null) {
    651                 boolean princpalMatch = false;
    652                 for (Principal princpal : subject.getPrincipals()) {
    653                     if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
    654                         princpalMatch = true;
    655                         break;
    656                     }
    657                 }
    658
    659                 if (!princpalMatch) {
    660                     throw new ForceAuthenticationException(
    661                             "Authenticated principal does not match previously authenticated principal");
    662                 }
    663             }
    664         }
    665     }
    666
    667     /**
    668      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
    669      * created.
    670      *
    671      * @param loginContext current login context
    672      * @param authenticationSubject subject created from the authentication method
    673      * @param authenticationMethod the method used to authenticate the subject
    674      * @param authenticationInstant the time of authentication
    675      * @param httpRequest current HTTP request
    676      * @param httpResponse current HTTP response
    677      */
    678     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
    679             String authenticationMethod, DateTime authenticationInstant, HttpServletRequest httpRequest,
    680             HttpServletResponse httpResponse) {
    681         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
    682         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
    683
    684         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    685         if (idpSession == null) {
    686             LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
    687             idpSession = (Session) sessionManager.createSession();
    688             loginContext.setSessionID(idpSession.getSessionID());
    689             addSessionCookie(httpRequest, httpResponse, idpSession);
    690         }
    691
    692         // Merge the information in the current session subject with the information from the
    693         // login handler subject
    694         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
    695
    696         // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
    697         // if not record the new information
    698         AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
    699                 authenticationMethod);
    700         if (authnMethodInfo == null || authenticationInstant != null) {
    701             LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
    702                     authenticationPrincipal.getName());
    703             LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
    704             DateTime authnInstant = authenticationInstant;
    705             if (authnInstant == null) {
    706                 authnInstant = new DateTime();
    707             }
    708             authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession.getSubject(), authenticationPrincipal,
    709                     authenticationMethod, authnInstant, loginHandler.getAuthenticationDuration());
    710         }
    711
    712         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
    713         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
    714         sessionManager.indexSession(idpSession, idpSession.getPrincipalName());
    715
    716         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
    717                 authnMethodInfo);
    718         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
    719     }
    720
    721     /**
    722      * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
    723      * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
    724      * the private credentials from both subjects, if not the new subject will not contain private credentials. If
    725      * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
    726      * from both subjects, if not the new subject will not contain public credentials.
    727      *
    728      * @param subject1 first subject to merge, may be null
    729      * @param subject2 second subject to merge, may be null
    730      *
    731      * @return subject containing the merged information
    732      */
    733     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
    734         if (subject1 == null && subject2 == null) {
    735             return new Subject();
    736         }
    737
    738         if (subject1 == null) {
    739             return subject2;
    740         }
    741
    742         if (subject2 == null) {
    743             return subject1;
    744         }
    745
    746         Set<Principal> principals = new HashSet<Principal>(3);
    747         principals.addAll(subject1.getPrincipals());
    748         principals.addAll(subject2.getPrincipals());
    749
    750         Set<Object> publicCredentials = new HashSet<Object>(3);
    751         if (retainSubjectsPublicCredentials) {
    752             LOG.debug("Merging in subjects public credentials");
    753             publicCredentials.addAll(subject1.getPublicCredentials());
    754             publicCredentials.addAll(subject2.getPublicCredentials());
    755         }
    756
    757         Set<Object> privateCredentials = new HashSet<Object>(3);
    758         if (retainSubjectsPrivateCredentials) {
    759             LOG.debug("Merging in subjects private credentials");
    760             privateCredentials.addAll(subject1.getPrivateCredentials());
    761             privateCredentials.addAll(subject2.getPrivateCredentials());
    762         }
    763
    764         return new Subject(false, principals, publicCredentials, privateCredentials);
    765     }
    766
    767     /**
    768      * Adds an IdP session cookie to the outbound response.
    769      *
    770      * @param httpRequest current request
    771      * @param httpResponse current response
    772      * @param userSession user's session
    773      */
    774     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
    775             Session userSession) {
    776         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
    777
    778         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
    779         byte[] sessionId = userSession.getSessionID().getBytes();
    780
    781         String signature = null;
    782         try {
    783             MessageDigest digester = MessageDigest.getInstance("SHA");
    784             digester.update(userSession.getSessionSecret());
    785             digester.update(remoteAddress);
    786             digester.update(sessionId);
    787             signature = Base64.encodeBytes(digester.digest());
    788         } catch (GeneralSecurityException e) {
    789             LOG.error("Unable to compute signature over session cookie material", e);
    790         }
    791
    792         LOG.debug("Adding IdP session cookie to HTTP response");
    793         StringBuilder cookieValue = new StringBuilder();
    794         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
    795         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
    796         cookieValue.append(signature);
    797
    798         String cookieDomain = HttpServletHelper.getCookieDomain(context);
    799
    800         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
    801         sessionCookie.setVersion(1);
    802         if (cookieDomain != null) {
    803             sessionCookie.setDomain(cookieDomain);
    804         }
    805         sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
    806         sessionCookie.setSecure(httpRequest.isSecure());
    807         httpResponse.addCookie(sessionCookie);
    808     }
    809 }