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.HashMap;
    25  import java.util.HashSet;
    26  import java.util.Iterator;
    27  import java.util.Map;
    28  import java.util.Set;
    29  import java.util.Map.Entry;
    30
    31  import javax.security.auth.Subject;
    32  import javax.servlet.RequestDispatcher;
    33  import javax.servlet.ServletConfig;
    34  import javax.servlet.ServletContext;
    35  import javax.servlet.ServletException;
    36  import javax.servlet.http.Cookie;
    37  import javax.servlet.http.HttpServlet;
    38  import javax.servlet.http.HttpServletRequest;
    39  import javax.servlet.http.HttpServletResponse;
    40
    41  import org.joda.time.DateTime;
    42  import org.opensaml.saml2.core.AuthnContext;
    43  import org.opensaml.util.storage.StorageService;
    44  import org.opensaml.ws.transport.http.HTTPTransportUtils;
    45  import org.opensaml.xml.util.Base64;
    46  import org.opensaml.xml.util.DatatypeHelper;
    47  import org.slf4j.Logger;
    48  import org.slf4j.LoggerFactory;
    49  import org.slf4j.helpers.MessageFormatter;
    50
    51  import edu.internet2.middleware.shibboleth.common.session.SessionManager;
    52  import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
    53  import edu.internet2.middleware.shibboleth.idp.authn.provider.PreviousSessionLoginHandler;
    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         // Remove the login context from the replicated store and bind it to the request
    168         HttpServletHelper.unbindLoginContext(storageService, context, httpRequest, httpResponse);
    169         HttpServletHelper.bindLoginContext(loginContext, httpRequest);
    170         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
    171         forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
    172     }
    173
    174     /**
    175      * Forwards a request to the given path.
    176      *
    177      * @param forwardPath path to forward the request to
    178      * @param httpRequest current HTTP request
    179      * @param httpResponse current HTTP response
    180      */
    181     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
    182             HttpServletResponse httpResponse) {
    183         try {
    184             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
    185             dispatcher.forward(httpRequest, httpResponse);
    186             return;
    187         } catch (IOException e) {
    188             LOG.error("Unable to return control back to authentication engine", e);
    189         } catch (ServletException e) {
    190             LOG.error("Unable to return control back to authentication engine", e);
    191         }
    192     }
    193
    194     /** {@inheritDoc} */
    195     @SuppressWarnings("unchecked")
    196     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
    197             IOException {
    198         LOG.debug("Processing incoming request");
    199
    200         if (httpResponse.isCommitted()) {
    201             LOG.error("HTTP Response already committed");
    202         }
    203
    204         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, getServletContext(), httpRequest);
    205         if (loginContext == null) {
    206             LOG.error("Incoming request does not have attached login context");
    207             throw new ServletException("Incoming request does not have attached login context");
    208         }
    209
    210         if (!loginContext.getAuthenticationAttempted()) {
    211             startUserAuthentication(loginContext, httpRequest, httpResponse);
    212         } else {
    213             completeAuthentication(loginContext, httpRequest, httpResponse);
    214         }
    215     }
    216
    217     /**
    218      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
    219      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
    220      * depending on whether passive authentication is required.
    221      *
    222      * @param loginContext current login context
    223      * @param httpRequest current HTTP request
    224      * @param httpResponse current HTTP response
    225      */
    226     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    227             HttpServletResponse httpResponse) {
    228         LOG.debug("Beginning user authentication process.");
    229         try {
    230             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    231             if (idpSession != null) {
    232                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
    233             }
    234
    235             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
    236             LOG.debug("Possible authentication handlers for this request: {}", possibleLoginHandlers);
    237
    238             // Filter out possible candidate login handlers by forced and passive authentication requirements
    239             if (loginContext.isForceAuthRequired()) {
    240                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
    241             }
    242
    243             if (loginContext.isPassiveAuthRequired()) {
    244                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
    245             }
    246             LOG.debug("Possible authentication handlers after filtering: {}", possibleLoginHandlers);
    247
    248             LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
    249
    250             LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
    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      * Selects a login handler from a list of possible login handlers that could be used for the request.
    266      *
    267      * @param possibleLoginHandlers list of possible login handlers that could be used for the request
    268      * @param loginContext current login context
    269      * @param idpSession current IdP session, if one exists
    270      *
    271      * @return the login handler to use for this request
    272      *
    273      * @throws AuthenticationException thrown if no handler can be used for this request
    274      */
    275     protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
    276             LoginContext loginContext, Session idpSession) throws AuthenticationException {
    277         LOG.debug("Selecting appropriate login handler for request");
    278         LoginHandler loginHandler;
    279         if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    280             LOG.debug("Using previous session login handler");
    281             loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    282
    283             for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
    284                 if (authnMethod.isExpired()) {
    285                     continue;
    286                 }
    287
    288                 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
    289                         || loginContext.getRequestedAuthenticationMethods().contains(
    290                                 authnMethod.getAuthenticationMethod())) {
    291                     LOG.debug("Basing previous session authentication on active authentication method {}", authnMethod
    292                             .getAuthenticationMethod());
    293                     loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
    294                     loginContext.setAuthenticationMethodInformation(authnMethod);
    295                     break;
    296                 }
    297             }
    298         } else {
    299             possibleLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    300             if (possibleLoginHandlers.isEmpty()) {
    301                 LOG.info("No authentication mechanism available for use with relying party '{}'", loginContext
    302                         .getRelyingPartyId());
    303                 throw new AuthenticationException();
    304             }
    305
    306             if (loginContext.getDefaultAuthenticationMethod() != null
    307                     && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
    308                 loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
    309                 loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
    310             } else {
    311                 Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
    312                 loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
    313                 loginHandler = chosenLoginHandler.getValue();
    314             }
    315         }
    316
    317         return loginHandler;
    318     }
    319
    320     /**
    321      * Determines which configured login handlers will support the requested authentication methods.
    322      *
    323      * @param loginContext current login context
    324      * @param idpSession current user's session, or null if they don't have one
    325      *
    326      * @return login methods that may be used to authenticate the user
    327      *
    328      * @throws AuthenticationException thrown if no login handler meets the given requirements
    329      */
    330     protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
    331             throws AuthenticationException {
    332         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(handlerManager
    333                 .getLoginHandlers());
    334         LOG.debug("Filtering configured login handlers by requested athentication methods.");
    335         LOG.debug("Configured LoginHandlers: {}", supportedLoginHandlers);
    336         LOG.debug("Requested authentication methods: {}", loginContext.getRequestedAuthenticationMethods());
    337
    338         // If no preferences Authn method preference is given, then we're free to use any
    339         if (loginContext.getRequestedAuthenticationMethods().isEmpty()) {
    340             LOG.trace("No preference given for authentication methods");
    341             return supportedLoginHandlers;
    342         }
    343
    344         // If the previous session handler is configured, the user has an existing session, and the SP requested
    345         // that a certain set of authentication methods be used then we need to check to see if the user has
    346         // authenticated with one or more of those methods, if not we can't use the previous session handler
    347         if (supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX) && idpSession != null
    348                 && loginContext.getRequestedAuthenticationMethods() != null) {
    349             boolean retainPreviousSession = false;
    350
    351             Map<String, AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods();
    352             for (String currentAuthnMethod : currentAuthnMethods.keySet()) {
    353                 if (loginContext.getRequestedAuthenticationMethods().contains(currentAuthnMethod)) {
    354                     retainPreviousSession = true;
    355                     break;
    356                 }
    357             }
    358
    359             if (!retainPreviousSession) {
    360                 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    361             }
    362         }
    363
    364         // Otherwise we need to filter all the mechanism supported by the IdP so that only the request types are left
    365         // Previous session handler is a special case, we always to keep that around if it's configured
    366         Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet().iterator();
    367         Entry<String, LoginHandler> supportedLoginHandler;
    368         while (supportedLoginHandlerItr.hasNext()) {
    369             supportedLoginHandler = supportedLoginHandlerItr.next();
    370             if (!supportedLoginHandler.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
    371                     && !loginContext.getRequestedAuthenticationMethods().contains(supportedLoginHandler.getKey())) {
    372                 supportedLoginHandlerItr.remove();
    373                 continue;
    374             }
    375         }
    376
    377         if (supportedLoginHandlers.isEmpty()) {
    378             LOG.warn("No authentication method, requested by the service provider, is supported");
    379             throw new AuthenticationException(
    380                     "No authentication method, requested by the service provider, is supported");
    381         }
    382
    383         return supportedLoginHandlers;
    384     }
    385
    386     /**
    387      * Filters out any login handler based on the requirement for forced authentication.
    388      *
    389      * During forced authentication any handler that has not previously been used to authenticate the user or any
    390      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
    391      *
    392      * @param idpSession user's current IdP session
    393      * @param loginContext current login context
    394      * @param loginHandlers login handlers to filter
    395      *
    396      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
    397      */
    398     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
    399             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
    400         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
    401
    402         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
    403         if (idpSession != null) {
    404             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
    405         }
    406
    407         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    408
    409         LoginHandler loginHandler;
    410         for (AuthenticationMethodInformation activeMethod : activeMethods) {
    411             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
    412             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
    413                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
    414                     loginHandlers.remove(handlerSupportedMethods);
    415                 }
    416             }
    417         }
    418
    419         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
    420                 loginHandlers);
    421
    422         if (loginHandlers.isEmpty()) {
    423             LOG.info("Force authentication requested but no login handlers available to support it");
    424             throw new ForceAuthenticationException();
    425         }
    426     }
    427
    428     /**
    429      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
    430      * authentication is required.
    431      *
    432      * @param idpSession user's current IdP session
    433      * @param loginContext current login context
    434      * @param loginHandlers login handlers to filter
    435      *
    436      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
    437      */
    438     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
    439             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
    440         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
    441
    442         if (idpSession == null) {
    443             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    444         }
    445
    446         LoginHandler loginHandler;
    447         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
    448         while (authnMethodItr.hasNext()) {
    449             loginHandler = authnMethodItr.next().getValue();
    450             if (!loginHandler.supportsPassive()) {
    451                 authnMethodItr.remove();
    452             }
    453         }
    454
    455         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
    456                 loginHandlers);
    457
    458         if (loginHandlers.isEmpty()) {
    459             LOG.warn("Passive authentication required but no login handlers available to support it");
    460             throw new PassiveAuthenticationException();
    461         }
    462     }
    463
    464     /**
    465      * Completes the authentication process.
    466      *
    467      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
    468      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
    469      * recorded and finally control is returned back to the profile handler.
    470      *
    471      * @param loginContext current login context
    472      * @param httpRequest current HTTP request
    473      * @param httpResponse current HTTP response
    474      */
    475     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    476             HttpServletResponse httpResponse) {
    477         LOG.debug("Completing user authentication process");
    478
    479         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    480
    481         try {
    482             // We allow a login handler to override the authentication method in the
    483             // event that it supports multiple methods
    484             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    485                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
    486             if (actualAuthnMethod != null) {
    487                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
    488                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
    489                     String msg = MessageFormatter
    490                             .format(
    491                                     "Relying patry required an authentication method of '{}' but the login handler performed '{}'",
    492                                     loginContext.getRequestedAuthenticationMethods(), actualAuthnMethod);
    493                     LOG.error(msg);
    494                     throw new AuthenticationException(msg);
    495                 }
    496             } else {
    497                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
    498             }
    499
    500             // Check to make sure the login handler did the right thing
    501             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
    502
    503             // Get the Subject from the request. If force authentication was required then make sure the
    504             // Subject identifies the same user that authenticated before
    505             Subject subject = getLoginHandlerSubject(httpRequest);
    506             if (loginContext.isForceAuthRequired()) {
    507                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
    508             }
    509
    510             loginContext.setPrincipalAuthenticated(true);
    511             updateUserSession(loginContext, subject, actualAuthnMethod, httpRequest, httpResponse);
    512             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(), loginContext
    513                     .getAuthenticationMethod());
    514         } catch (AuthenticationException e) {
    515             LOG.error("Authentication failed with the error:", e);
    516             loginContext.setPrincipalAuthenticated(false);
    517             loginContext.setAuthenticationFailure(e);
    518         }
    519
    520         returnToProfileHandler(httpRequest, httpResponse);
    521     }
    522
    523     /**
    524      * Validates that the authentication was successfully performed by the login handler. An authentication is
    525      * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
    526      * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
    527      * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
    528      *
    529      * @param loginContext current login context
    530      * @param httpRequest current HTTP request
    531      * @param authenticationMethod the authentication method used to authenticate the user
    532      *
    533      * @throws AuthenticationException thrown if the authentication was not successful
    534      */
    535     protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    536             String authenticationMethod) throws AuthenticationException {
    537         LOG.debug("Validating authentication was performed successfully");
    538
    539         if (authenticationMethod == null) {
    540             LOG.error("No authentication method reported by login handler.");
    541             throw new AuthenticationException("No authentication method reported by login handler.");
    542         }
    543
    544         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    545                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
    546         if (errorMessage != null) {
    547             LOG.error("Error returned from login handler for authentication method {}:\n{}", loginContext
    548                     .getAttemptedAuthnMethod(), errorMessage);
    549             throw new AuthenticationException(errorMessage);
    550         }
    551
    552         AuthenticationException authnException = (AuthenticationException) httpRequest
    553                 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
    554         if (authnException != null) {
    555             throw authnException;
    556         }
    557
    558         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    559         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    560         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    561                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    562
    563         if (subject == null && principal == null && principalName == null) {
    564             LOG.error("No user identified by login handler.");
    565             throw new AuthenticationException("No user identified by login handler.");
    566         }
    567     }
    568
    569     /**
    570      * Gets the subject from the request coming back from the login handler.
    571      *
    572      * @param httpRequest request coming back from the login handler
    573      *
    574      * @return the {@link Subject} created from the request
    575      *
    576      * @throws AuthenticationException thrown if no subject can be retrieved from the request
    577      */
    578     protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
    579         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
    580         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
    581         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    582                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
    583
    584         if (subject == null && (principal != null || principalName != null)) {
    585             subject = new Subject();
    586             if (principal == null) {
    587                 principal = new UsernamePrincipal(principalName);
    588             }
    589             subject.getPrincipals().add(principal);
    590         }
    591
    592         return subject;
    593     }
    594
    595     /**
    596      * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
    597      * principal name that is equal to the principal name associated with the authentication method. If this is the
    598      * first time the subject has authenticated with this method than this check always passes.
    599      *
    600      * @param idpSession user's IdP session
    601      * @param authnMethod method used to authenticate the user
    602      * @param subject subject that was authenticated
    603      *
    604      * @throws AuthenticationException thrown if this check fails
    605      */
    606     protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
    607             throws AuthenticationException {
    608         if (idpSession != null) {
    609             AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
    610             if (authnMethodInfo != null) {
    611                 boolean princpalMatch = false;
    612                 for (Principal princpal : subject.getPrincipals()) {
    613                     if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
    614                         princpalMatch = true;
    615                         break;
    616                     }
    617                 }
    618
    619                 if (!princpalMatch) {
    620                     throw new ForceAuthenticationException(
    621                             "Authenticated principal does not match previously authenticated principal");
    622                 }
    623             }
    624         }
    625     }
    626
    627     /**
    628      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
    629      * created.
    630      *
    631      * @param loginContext current login context
    632      * @param authenticationSubject subject created from the authentication method
    633      * @param authenticationMethod the method used to authenticate the subject
    634      * @param httpRequest current HTTP request
    635      * @param httpResponse current HTTP response
    636      */
    637     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
    638             String authenticationMethod, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
    639         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
    640         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
    641
    642         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    643         if (idpSession == null) {
    644             LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
    645             idpSession = (Session) sessionManager.createSession();
    646             loginContext.setSessionID(idpSession.getSessionID());
    647             addSessionCookie(httpRequest, httpResponse, idpSession);
    648         }
    649
    650         // Merge the information in the current session subject with the information from the
    651         // login handler subject
    652         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
    653
    654         // Check if an existing authentication method was used (i.e. SSO occurred), if not record the new information
    655         AuthenticationMethodInformation authnMethodInfo = loginContext.getAuthenticationMethodInformation();
    656         if(authnMethodInfo == null || !authnMethodInfo.getAuthenticationMethod().equals(authenticationMethod)){
    657             LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
    658                     authenticationPrincipal.getName());
    659             LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
    660             authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession
    661                     .getSubject(), authenticationPrincipal, authenticationMethod, new DateTime(), loginHandler
    662                     .getAuthenticationDuration());
    663         }
    664
    665         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
    666         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
    667         sessionManager.indexSession(idpSession, authnMethodInfo.getAuthenticationPrincipal().getName());
    668
    669         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
    670                 authnMethodInfo);
    671         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
    672     }
    673
    674     /**
    675      * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
    676      * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
    677      * the private credentials from both subjects, if not the new subject will not contain private credentials. If
    678      * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
    679      * from both subjects, if not the new subject will not contain public credentials.
    680      *
    681      * @param subject1 first subject to merge, may be null
    682      * @param subject2 second subject to merge, may be null
    683      *
    684      * @return subject containing the merged information
    685      */
    686     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
    687         if (subject1 == null && subject2 == null) {
    688             return new Subject();
    689         }
    690
    691         if (subject1 == null) {
    692             return subject2;
    693         }
    694
    695         if (subject2 == null) {
    696             return subject1;
    697         }
    698
    699         Set<Principal> principals = new HashSet<Principal>(3);
    700         principals.addAll(subject1.getPrincipals());
    701         principals.addAll(subject2.getPrincipals());
    702
    703         Set<Object> publicCredentials = new HashSet<Object>(3);
    704         if (retainSubjectsPublicCredentials) {
    705             LOG.debug("Merging in subjects public credentials");
    706             publicCredentials.addAll(subject1.getPublicCredentials());
    707             publicCredentials.addAll(subject2.getPublicCredentials());
    708         }
    709
    710         Set<Object> privateCredentials = new HashSet<Object>(3);
    711         if (retainSubjectsPrivateCredentials) {
    712             LOG.debug("Merging in subjects private credentials");
    713             privateCredentials.addAll(subject1.getPrivateCredentials());
    714             privateCredentials.addAll(subject2.getPrivateCredentials());
    715         }
    716
    717         return new Subject(false, principals, publicCredentials, privateCredentials);
    718     }
    719
    720     /**
    721      * Adds an IdP session cookie to the outbound response.
    722      *
    723      * @param httpRequest current request
    724      * @param httpResponse current response
    725      * @param userSession user's session
    726      */
    727     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
    728             Session userSession) {
    729         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
    730
    731         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
    732         byte[] sessionId = userSession.getSessionID().getBytes();
    733
    734         String signature = null;
    735         try {
    736             MessageDigest digester = MessageDigest.getInstance("SHA");
    737             digester.update(userSession.getSessionSecret());
    738             digester.update(remoteAddress);
    739             digester.update(sessionId);
    740             signature = Base64.encodeBytes(digester.digest());
    741         } catch (GeneralSecurityException e) {
    742             LOG.error("Unable to compute signature over session cookie material", e);
    743         }
    744
    745         LOG.debug("Adding IdP session cookie to HTTP response");
    746         StringBuilder cookieValue = new StringBuilder();
    747         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
    748         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
    749         cookieValue.append(signature);
    750
    751         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
    752         sessionCookie.setVersion(1);
    753         sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
    754         sessionCookie.setSecure(httpRequest.isSecure());
    755         httpResponse.addCookie(sessionCookie);
    756     }
    757 }