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.warn("No login context available, unable to proceed with authentication");
    210             forwardRequest("/error.jsp", httpRequest, httpResponse);
    211             return;
    212         }
    213
    214         if (!loginContext.getAuthenticationAttempted()) {
    215             startUserAuthentication(loginContext, httpRequest, httpResponse);
    216         } else {
    217             completeAuthentication(loginContext, httpRequest, httpResponse);
    218         }
    219     }
    220
    221     /**
    222      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
    223      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
    224      * depending on whether passive authentication is required.
    225      *
    226      * @param loginContext current login context
    227      * @param httpRequest current HTTP request
    228      * @param httpResponse current HTTP response
    229      */
    230     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    231             HttpServletResponse httpResponse) {
    232         LOG.debug("Beginning user authentication process.");
    233         try {
    234             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    235             if (idpSession != null) {
    236                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
    237             }
    238
    239             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
    240
    241             // Filter out possible candidate login handlers by forced and passive authentication requirements
    242             if (loginContext.isForceAuthRequired()) {
    243                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
    244             }
    245
    246             if (loginContext.isPassiveAuthRequired()) {
    247                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
    248             }
    249
    250             LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
    251             loginContext.setAuthenticationAttempted();
    252             loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
    253
    254             // Send the request to the login handler
    255             HttpServletHelper.bindLoginContext(loginContext, storageService, getServletContext(), httpRequest,
    256                     httpResponse);
    257             loginHandler.login(httpRequest, httpResponse);
    258         } catch (AuthenticationException e) {
    259             loginContext.setAuthenticationFailure(e);
    260             returnToProfileHandler(httpRequest, httpResponse);
    261         }
    262     }
    263
    264     /**
    265      * Determines which configured login handlers will support the requested authentication methods.
    266      *
    267      * @param loginContext current login context
    268      * @param idpSession current user's session, or null if they don't have one
    269      *
    270      * @return login methods that may be used to authenticate the user
    271      *
    272      * @throws AuthenticationException thrown if no login handler meets the given requirements
    273      */
    274     protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
    275             throws AuthenticationException {
    276         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(
    277                 handlerManager.getLoginHandlers());
    278         LOG.debug("Filtering configured LoginHandlers: {}", supportedLoginHandlers);
    279
    280         // First, if the service provider requested a particular authentication method, filter out everything but
    281         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
    282         if (requestedMethods != null && !requestedMethods.isEmpty()) {
    283             LOG.debug("Filtering possible login handlers by requested authentication methods: {}", requestedMethods);
    284             Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet()
    285                     .iterator();
    286             Entry<String, LoginHandler> supportedLoginHandlerEntry;
    287             while (supportedLoginHandlerItr.hasNext()) {
    288                 supportedLoginHandlerEntry = supportedLoginHandlerItr.next();
    289                 if (!supportedLoginHandlerEntry.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
    290                         && !requestedMethods.contains(supportedLoginHandlerEntry.getKey())) {
    291                     LOG.debug(
    292                             "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
    293                             supportedLoginHandlerEntry.getKey());
    294                     supportedLoginHandlerItr.remove();
    295                 }
    296             }
    297         }
    298
    299         // Next, determine, if present, if the previous session handler can be used
    300         filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
    301
    302         if (supportedLoginHandlers.isEmpty()) {
    303             LOG.warn("No authentication method, requested by the service provider, is supported");
    304             throw new AuthenticationException(
    305                     "No authentication method, requested by the service provider, is supported");
    306         }
    307
    308         return supportedLoginHandlers;
    309     }
    310
    311     /**
    312      * Filters out the previous session login handler if there is no existing IdP session, no active authentication
    313      * methods, or if at least one of the active authentication methods do not match the requested authentication
    314      * methods.
    315      *
    316      * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
    317      * @param idpSession current IdP session, may be null if no session currently exists
    318      * @param loginContext current login context, never null
    319      */
    320     protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
    321             Session idpSession, LoginContext loginContext) {
    322         if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    323             return;
    324         }
    325
    326         if (idpSession == null) {
    327             LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
    328             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    329             return;
    330         }
    331         Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
    332                 .values();
    333
    334         Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
    335         while (methodItr.hasNext()) {
    336             AuthenticationMethodInformation info = methodItr.next();
    337             if (info.isExpired()) {
    338                 methodItr.remove();
    339             }
    340         }
    341         if (currentAuthnMethods.isEmpty()) {
    342             LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
    343             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    344             return;
    345         }
    346
    347         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
    348         if (requestedMethods != null && !requestedMethods.isEmpty()) {
    349             boolean retainPreviousSession = false;
    350             for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
    351                 if (loginContext.getRequestedAuthenticationMethods().contains(
    352                         currentAuthnMethod.getAuthenticationMethod())) {
    353                     retainPreviousSession = true;
    354                     break;
    355                 }
    356             }
    357
    358             if (!retainPreviousSession) {
    359                 LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
    360                 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    361                 return;
    362             }
    363         }
    364     }
    365
    366     /**
    367      * Filters out any login handler based on the requirement for forced authentication.
    368      *
    369      * During forced authentication any handler that has not previously been used to authenticate the user or any
    370      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
    371      *
    372      * @param idpSession user's current IdP session
    373      * @param loginContext current login context
    374      * @param loginHandlers login handlers to filter
    375      *
    376      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
    377      */
    378     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
    379             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
    380         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
    381
    382         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
    383         if (idpSession != null) {
    384             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
    385         }
    386
    387         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    388
    389         LoginHandler loginHandler;
    390         for (AuthenticationMethodInformation activeMethod : activeMethods) {
    391             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
    392             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
    393                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
    394                     LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
    395                             .getClass().getName());
    396                     loginHandlers.remove(handlerSupportedMethods);
    397                 }
    398             }
    399         }
    400
    401         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
    402                 loginHandlers);
    403
    404         if (loginHandlers.isEmpty()) {
    405             LOG.info("Force authentication requested but no login handlers available to support it");
    406             throw new ForceAuthenticationException();
    407         }
    408     }
    409
    410     /**
    411      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
    412      * authentication is required.
    413      *
    414      * @param idpSession user's current IdP session
    415      * @param loginContext current login context
    416      * @param loginHandlers login handlers to filter
    417      *
    418      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
    419      */
    420     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
    421             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
    422         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
    423
    424         if (idpSession == null) {
    425             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    426         }
    427
    428         LoginHandler loginHandler;
    429         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
    430         while (authnMethodItr.hasNext()) {
    431             loginHandler = authnMethodItr.next().getValue();
    432             if (!loginHandler.supportsPassive()) {
    433                 authnMethodItr.remove();
    434             }
    435         }
    436
    437         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
    438                 loginHandlers);
    439
    440         if (loginHandlers.isEmpty()) {
    441             LOG.warn("Passive authentication required but no login handlers available to support it");
    442             throw new PassiveAuthenticationException();
    443         }
    444     }
    445
    446     /**
    447      * Selects a login handler from a list of possible login handlers that could be used for the request.
    448      *
    449      * @param possibleLoginHandlers list of possible login handlers that could be used for the request
    450      * @param loginContext current login context
    451      * @param idpSession current IdP session, if one exists
    452      *
    453      * @return the login handler to use for this request
    454      *
    455      * @throws AuthenticationException thrown if no handler can be used for this request
    456      */
    457     protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
    458             LoginContext loginContext, Session idpSession) throws AuthenticationException {
    459         LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
    460         LoginHandler loginHandler;
    461         if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    462             LOG.debug("Authenticating user with previous session LoginHandler");
    463             loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    464
    465             for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
    466                 if (authnMethod.isExpired()) {
    467                     continue;
    468                 }
    469
    470                 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
    471                         || loginContext.getRequestedAuthenticationMethods().contains(
    472                                 authnMethod.getAuthenticationMethod())) {
    473                     LOG.debug("Basing previous session authentication on active authentication method {}",
    474                             authnMethod.getAuthenticationMethod());
    475                     loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
    476                     loginContext.setAuthenticationMethodInformation(authnMethod);
    477                     return loginHandler;
    478                 }
    479             }
    480         }
    481
    482         if (loginContext.getDefaultAuthenticationMethod() != null
    483                 && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
    484             loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
    485             loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
    486         } else {
    487             Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
    488             loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
    489             loginHandler = chosenLoginHandler.getValue();
    490         }
    491
    492         LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
    493         return loginHandler;
    494     }
    495
    496     /**
    497      * Completes the authentication process.
    498      *
    499      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
    500      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
    501      * recorded and finally control is returned back to the profile handler.
    502      *
    503      * @param loginContext current login context
    504      * @param httpRequest current HTTP request
    505      * @param httpResponse current HTTP response
    506      */
    507     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    508             HttpServletResponse httpResponse) {
    509         LOG.debug("Completing user authentication process");
    510
    511         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    512
    513         try {
    514             // We allow a login handler to override the authentication method in the
    515             // event that it supports multiple methods
    516             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    517                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
    518             if (actualAuthnMethod != null) {
    519                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
    520                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
    521                     String msg = "Relying patry required an authentication method of "
    522                             + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
    523                             + actualAuthnMethod;
    524                     LOG.error(msg);
    525                     throw new AuthenticationException(msg);
    526                 }
    527             } else {
    528                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
    529             }
    530
    531             // Check to make sure the login handler did the right thing
    532             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
    533
    534             // Check for an overridden authn instant.
    535             DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
    536
    537             // Get the Subject from the request. If force authentication was required then make sure the
    538             // Subject identifies the same user that authenticated before
    539             Subject subject = getLoginHandlerSubject(httpRequest);
    540             if (loginContext.isForceAuthRequired()) {
    541                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
    542
    543                 // Reset the authn instant.
    544                 if (actualAuthnInstant == null) {
    545                     actualAuthnInstant = new DateTime();
    546                 }
    547             }
    548
    549             loginContext.setPrincipalAuthenticated(true);
    550             updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
    551             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
    552                     loginContext.getAuthenticationMethod());
    553         } catch (AuthenticationException e) {
    554             LOG.error("Authentication failed with the error:", e);
    555             loginContext.setPrincipalAuthenticated(false);
    556             loginContext.setAuthenticationFailure(e);
    557         }
    558
    559         returnToProfileHandler(httpRequest, httpResponse);
    560     }
    561
    562     /**
    563      * Validates that the authentication was successfully performed by the login handler. An authentication is
    564      * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
    565      * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
    566      * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
    567      *
    568      * @param loginContext current login context
    569      * @param httpRequest current HTTP request
    570      * @param authenticationMethod the authentication method used to authenticate the user
    571      *
    572      * @throws AuthenticationException thrown if the authentication was not successful
    573      */
    574     protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    575             String authenticationMethod) throws AuthenticationException {
    576         LOG.debug("Validating authentication was performed successfully");
    577
    578         if (authenticationMethod == null) {
    579             LOG.error("No authentication method reported by login handler.");
    580             throw new AuthenticationException("No authentication method reported by login handler.");
    581         }
    582
    583         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    584                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
    585         if (errorMessage != null) {
    586             LOG.error("Error returned from login handler for authentication method {}:\n{}",
    587                     loginContext.getAttemptedAuthnMethod(), errorMessage);
    588             throw new AuthenticationException(errorMessage);
    589         }
    590
    591         AuthenticationException authnException = (AuthenticationException) httpRequest
    592                 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
    593         if (authnException != null) {
    594             throw authnException;
    595         }
    596
    597         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    598         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    599         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    600                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    601
    602         if (subject == null && principal == null && principalName == null) {
    603             LOG.error("No user identified by login handler.");
    604             throw new AuthenticationException("No user identified by login handler.");
    605         }
    606     }
    607
    608     /**
    609      * Gets the subject from the request coming back from the login handler.
    610      *
    611      * @param httpRequest request coming back from the login handler
    612      *
    613      * @return the {@link Subject} created from the request
    614      *
    615      * @throws AuthenticationException thrown if no subject can be retrieved from the request
    616      */
    617     protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
    618         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    619         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    620         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    621                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    622
    623         if (subject == null && (principal != null || principalName != null)) {
    624             subject = new Subject();
    625             if (principal == null) {
    626                 principal = new UsernamePrincipal(principalName);
    627             }
    628             subject.getPrincipals().add(principal);
    629         }
    630
    631         return subject;
    632     }
    633
    634     /**
    635      * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
    636      * principal name that is equal to the principal name associated with the authentication method. If this is the
    637      * first time the subject has authenticated with this method than this check always passes.
    638      *
    639      * @param idpSession user's IdP session
    640      * @param authnMethod method used to authenticate the user
    641      * @param subject subject that was authenticated
    642      *
    643      * @throws AuthenticationException thrown if this check fails
    644      */
    645     protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
    646             throws AuthenticationException {
    647         if (idpSession != null) {
    648             AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
    649             if (authnMethodInfo != null) {
    650                 boolean princpalMatch = false;
    651                 for (Principal princpal : subject.getPrincipals()) {
    652                     if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
    653                         princpalMatch = true;
    654                         break;
    655                     }
    656                 }
    657
    658                 if (!princpalMatch) {
    659                     throw new ForceAuthenticationException(
    660                             "Authenticated principal does not match previously authenticated principal");
    661                 }
    662             }
    663         }
    664     }
    665
    666     /**
    667      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
    668      * created.
    669      *
    670      * @param loginContext current login context
    671      * @param authenticationSubject subject created from the authentication method
    672      * @param authenticationMethod the method used to authenticate the subject
    673      * @param authenticationInstant the time of authentication
    674      * @param httpRequest current HTTP request
    675      * @param httpResponse current HTTP response
    676      */
    677     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
    678             String authenticationMethod, DateTime authenticationInstant, HttpServletRequest httpRequest,
    679             HttpServletResponse httpResponse) {
    680         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
    681         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
    682
    683         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    684         if (idpSession == null) {
    685             LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
    686             idpSession = (Session) sessionManager.createSession();
    687             loginContext.setSessionID(idpSession.getSessionID());
    688             addSessionCookie(httpRequest, httpResponse, idpSession);
    689         }
    690
    691         // Merge the information in the current session subject with the information from the
    692         // login handler subject
    693         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
    694
    695         // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
    696         // if not record the new information
    697         AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
    698                 authenticationMethod);
    699         if (authnMethodInfo == null || authenticationInstant != null) {
    700             LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
    701                     authenticationPrincipal.getName());
    702             LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
    703             DateTime authnInstant = authenticationInstant;
    704             if (authnInstant == null) {
    705                 authnInstant = new DateTime();
    706             }
    707             authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession.getSubject(), authenticationPrincipal,
    708                     authenticationMethod, authnInstant, loginHandler.getAuthenticationDuration());
    709         }
    710
    711         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
    712         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
    713         sessionManager.indexSession(idpSession, idpSession.getPrincipalName());
    714
    715         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
    716                 authnMethodInfo);
    717         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
    718     }
    719
    720     /**
    721      * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
    722      * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
    723      * the private credentials from both subjects, if not the new subject will not contain private credentials. If
    724      * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
    725      * from both subjects, if not the new subject will not contain public credentials.
    726      *
    727      * @param subject1 first subject to merge, may be null
    728      * @param subject2 second subject to merge, may be null
    729      *
    730      * @return subject containing the merged information
    731      */
    732     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
    733         if (subject1 == null && subject2 == null) {
    734             return new Subject();
    735         }
    736
    737         if (subject1 == null) {
    738             return subject2;
    739         }
    740
    741         if (subject2 == null) {
    742             return subject1;
    743         }
    744
    745         Set<Principal> principals = new HashSet<Principal>(3);
    746         principals.addAll(subject1.getPrincipals());
    747         principals.addAll(subject2.getPrincipals());
    748
    749         Set<Object> publicCredentials = new HashSet<Object>(3);
    750         if (retainSubjectsPublicCredentials) {
    751             LOG.debug("Merging in subjects public credentials");
    752             publicCredentials.addAll(subject1.getPublicCredentials());
    753             publicCredentials.addAll(subject2.getPublicCredentials());
    754         }
    755
    756         Set<Object> privateCredentials = new HashSet<Object>(3);
    757         if (retainSubjectsPrivateCredentials) {
    758             LOG.debug("Merging in subjects private credentials");
    759             privateCredentials.addAll(subject1.getPrivateCredentials());
    760             privateCredentials.addAll(subject2.getPrivateCredentials());
    761         }
    762
    763         return new Subject(false, principals, publicCredentials, privateCredentials);
    764     }
    765
    766     /**
    767      * Adds an IdP session cookie to the outbound response.
    768      *
    769      * @param httpRequest current request
    770      * @param httpResponse current response
    771      * @param userSession user's session
    772      */
    773     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
    774             Session userSession) {
    775         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
    776
    777         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
    778         byte[] sessionId = userSession.getSessionID().getBytes();
    779
    780         String signature = null;
    781         try {
    782             MessageDigest digester = MessageDigest.getInstance("SHA");
    783             digester.update(userSession.getSessionSecret());
    784             digester.update(remoteAddress);
    785             digester.update(sessionId);
    786             signature = Base64.encodeBytes(digester.digest());
    787         } catch (GeneralSecurityException e) {
    788             LOG.error("Unable to compute signature over session cookie material", e);
    789         }
    790
    791         LOG.debug("Adding IdP session cookie to HTTP response");
    792         StringBuilder cookieValue = new StringBuilder();
    793         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
    794         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
    795         cookieValue.append(signature);
    796
    797         String cookieDomain = HttpServletHelper.getCookieDomain(context);
    798
    799         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
    800         sessionCookie.setVersion(1);
    801         if (cookieDomain != null) {
    802             sessionCookie.setDomain(cookieDomain);
    803         }
    804         sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
    805         sessionCookie.setSecure(httpRequest.isSecure());
    806         httpResponse.addCookie(sessionCookie);
    807     }
    808 }