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.NoSuchAlgorithmException;
    23  import java.security.Principal;
    24  import java.util.ArrayList;
    25  import java.util.HashMap;
    26  import java.util.HashSet;
    27  import java.util.Iterator;
    28  import java.util.Map;
    29  import java.util.Set;
    30  import java.util.Map.Entry;
    31
    32  import javax.security.auth.Subject;
    33  import javax.servlet.RequestDispatcher;
    34  import javax.servlet.ServletConfig;
    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.common.IdentifierGenerator;
    43  import org.opensaml.common.impl.SecureRandomIdentifierGenerator;
    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
    61  /** Manager responsible for handling authentication requests. */
    62  public class AuthenticationEngine extends HttpServlet {
    63
    64      /** Name of the Servlet config init parameter that holds the partition name for login contexts. */
    65      public static final String LOGIN_CONTEXT_PARTITION_NAME_INIT_PARAM_NAME = "loginContextPartitionName";
    66
    67      /** Name of the Servlet config init parameter that holds lifetime of a login context in the storage service. */
    68      public static final String LOGIN_CONTEXT_LIFETIME_INIT_PARAM_NAME = "loginContextEntryLifetime";
    69
    70      /** Name of the IdP Cookie containing the IdP session ID. */
    71      public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
    72
    73      /** Name of the key under which to bind the storage service key for a login context. */
    74      public static final String LOGIN_CONTEXT_KEY_NAME = "_idp_authn_lc_key";
    75
    76      /** Serial version UID. */
    77      private static final long serialVersionUID = -8479060989001890156L;
    78
    79      /** Class logger. */
    80      private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
    81
    82      /** Storage service used to store {@link LoginContext}s while authentication is in progress. */
    83      private static StorageService<String, LoginContextEntry> storageService;
    84
    85      /** Name of the storage service partition used to store login contexts. */
    86      private static String loginContextPartitionName;
    87
    88      /** Lifetime of stored login contexts. */
    89      private static long loginContextEntryLifetime;
    90
    91      /** ID generator. */
    92      private static IdentifierGenerator idGen;
    93
    94      /** Profile handler manager. */
    95      private IdPProfileHandlerManager handlerManager;
    96
    97      /** Session manager. */
    98      private SessionManager<Session> sessionManager;
    99
    100     /** {@inheritDoc} */
    101     public void init(ServletConfig config) throws ServletException {
    102         super.init(config);
    103
    104         String handlerManagerId = config.getInitParameter("handlerManagerId");
    105         if (DatatypeHelper.isEmpty(handlerManagerId)) {
    106             handlerManagerId = "shibboleth.HandlerManager";
    107         }
    108         handlerManager = (IdPProfileHandlerManager) getServletContext().getAttribute(handlerManagerId);
    109
    110         String sessionManagerId = config.getInitParameter("sessionManagedId");
    111         if (DatatypeHelper.isEmpty(sessionManagerId)) {
    112             sessionManagerId = "shibboleth.SessionManager";
    113         }
    114         sessionManager = (SessionManager<Session>) getServletContext().getAttribute(sessionManagerId);
    115
    116         String storageServiceId = config.getInitParameter("storageServiceId");
    117         if (DatatypeHelper.isEmpty(storageServiceId)) {
    118             storageServiceId = "shibboleth.StorageService";
    119         }
    120         storageService = (StorageService<String, LoginContextEntry>) getServletContext().getAttribute(storageServiceId);
    121
    122         String partitionName = DatatypeHelper.safeTrimOrNullString(config
    123                 .getInitParameter(LOGIN_CONTEXT_PARTITION_NAME_INIT_PARAM_NAME));
    124         if (partitionName != null) {
    125             loginContextPartitionName = partitionName;
    126         } else {
    127             loginContextPartitionName = "loginContexts";
    128         }
    129
    130         String lifetime = DatatypeHelper.safeTrimOrNullString(config
    131                 .getInitParameter(LOGIN_CONTEXT_LIFETIME_INIT_PARAM_NAME));
    132         if (lifetime != null) {
    133             loginContextEntryLifetime = Long.parseLong(lifetime);
    134         } else {
    135             loginContextEntryLifetime = 1000 * 60 * 30;
    136         }
    137
    138         try {
    139             idGen = new SecureRandomIdentifierGenerator();
    140         } catch (NoSuchAlgorithmException e) {
    141             throw new ServletException("Error create random number generator", e);
    142         }
    143     }
    144
    145     /**
    146      * Retrieves a login context.
    147      *
    148      * @param httpRequest current HTTP request
    149      * @param removeFromStorageService whether the login context should be removed from the storage service as it is
    150      *            retrieved
    151      *
    152      * @return the login context or null if one is not available (e.g. because it has expired)
    153      */
    154     protected static LoginContext retrieveLoginContext(HttpServletRequest httpRequest, boolean removeFromStorageService) {
    155         // When the login context comes from the profile handlers its attached to the request
    156         // Prior to the authentication engine handing control over to a login handler it stores
    157         // the login context into the storage service so that the login handlers do not have to
    158         // maintain a reference to the context and return it to the engine.
    159         LoginContext loginContext = (LoginContext) httpRequest.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
    160         if (loginContext != null) {
    161             LOG.trace("Login context retrieved from HTTP request attribute");
    162             return loginContext;
    163         }
    164
    165         String contextId = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    166                 .getAttribute(LOGIN_CONTEXT_KEY_NAME));
    167
    168         if (contextId == null) {
    169             Cookie[] requestCookies = httpRequest.getCookies();
    170             if (requestCookies != null) {
    171                 for (Cookie requestCookie : requestCookies) {
    172                     if (DatatypeHelper.safeEquals(requestCookie.getName(), LOGIN_CONTEXT_KEY_NAME)) {
    173                         LOG.trace("Located cookie with login context key");
    174                         contextId = requestCookie.getValue();
    175                         break;
    176                     }
    177                 }
    178             }
    179         }
    180
    181         LOG.trace("Using login context key {} to look up login context", contextId);
    182         LoginContextEntry entry;
    183         if (removeFromStorageService) {
    184             entry = storageService.remove(loginContextPartitionName, contextId);
    185         } else {
    186             entry = storageService.get(loginContextPartitionName, contextId);
    187         }
    188         if (entry == null) {
    189             LOG.trace("No entry for login context found in storage service.");
    190             return null;
    191         } else if (entry.isExpired()) {
    192             LOG.trace("Login context entry found in storage service but it was expired.");
    193             return null;
    194         } else {
    195             LOG.trace("Login context entry found in storage service.");
    196             return entry.getLoginContext();
    197         }
    198     }
    199
    200     /**
    201      * Returns control back to the authentication engine.
    202      *
    203      * @param httpRequest current HTTP request
    204      * @param httpResponse current HTTP response
    205      */
    206     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
    207         LOG.debug("Returning control to authentication engine");
    208         LoginContext loginContext = retrieveLoginContext(httpRequest, false);
    209         if (loginContext == null) {
    210             LOG.error("No login context available, unable to return to authentication engine");
    211             forwardRequest("/idp-error.jsp", httpRequest, httpResponse);
    212         } else {
    213             forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
    214         }
    215     }
    216
    217     /**
    218      * Returns control back to the profile handler that invoked the authentication engine.
    219      *
    220      * @param loginContext current login context
    221      * @param httpRequest current HTTP request
    222      * @param httpResponse current HTTP response
    223      */
    224     public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
    225             HttpServletResponse httpResponse) {
    226         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
    227         httpRequest.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
    228
    229         // Cleanup this cookie
    230         Cookie lcKeyCookie = new Cookie(LOGIN_CONTEXT_KEY_NAME, "");
    231         lcKeyCookie.setMaxAge(0);
    232         httpResponse.addCookie(lcKeyCookie);
    233
    234         forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
    235     }
    236
    237     /**
    238      * Forwards a request to the given path.
    239      *
    240      * @param forwardPath path to forward the request to
    241      * @param httpRequest current HTTP request
    242      * @param httpResponse current HTTP response
    243      */
    244     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
    245             HttpServletResponse httpResponse) {
    246         try {
    247             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
    248             dispatcher.forward(httpRequest, httpResponse);
    249             return;
    250         } catch (IOException e) {
    251             LOG.error("Unable to return control back to authentication engine", e);
    252         } catch (ServletException e) {
    253             LOG.error("Unable to return control back to authentication engine", e);
    254         }
    255     }
    256
    257     /** {@inheritDoc} */
    258     @SuppressWarnings("unchecked")
    259     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
    260             IOException {
    261         LOG.debug("Processing incoming request");
    262
    263         if (httpResponse.isCommitted()) {
    264             LOG.error("HTTP Response already committed");
    265         }
    266
    267         LoginContext loginContext = retrieveLoginContext(httpRequest, true);
    268         if (loginContext == null) {
    269             LOG.error("Incoming request does not have attached login context");
    270             throw new ServletException("Incoming request does not have attached login context");
    271         }
    272
    273         if (!loginContext.getAuthenticationAttempted()) {
    274             startUserAuthentication(loginContext, httpRequest, httpResponse);
    275         } else {
    276             completeAuthentication(loginContext, httpRequest, httpResponse);
    277         }
    278     }
    279
    280     /**
    281      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
    282      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
    283      * depending on whether passive authentication is required.
    284      *
    285      * @param loginContext current login context
    286      * @param httpRequest current HTTP request
    287      * @param httpResponse current HTTP response
    288      */
    289     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    290             HttpServletResponse httpResponse) {
    291         LOG.debug("Beginning user authentication process");
    292         try {
    293             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    294             if (idpSession != null) {
    295                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
    296             }
    297
    298             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(loginContext);
    299             LOG.debug("Possible authentication handlers for this request: {}", possibleLoginHandlers);
    300
    301             // Filter out possible candidate login handlers by forced and passive authentication requirements
    302             if (loginContext.isForceAuthRequired()) {
    303                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
    304             }
    305
    306             if (loginContext.isPassiveAuthRequired()) {
    307                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
    308             }
    309
    310             // If the user already has a session and its usage is acceptable than use it
    311             // otherwise just use the first candidate login handler
    312             LOG.debug("Possible authentication handlers after filtering: {}", possibleLoginHandlers);
    313             LoginHandler loginHandler;
    314             if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
    315                 loginContext.setAttemptedAuthnMethod(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    316                 loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    317             } else {
    318                 possibleLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    319                 Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
    320                 loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
    321                 loginHandler = chosenLoginHandler.getValue();
    322             }
    323
    324             // Send the request to the login handler
    325             LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
    326             loginContext.setAuthenticationAttempted();
    327             loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
    328             storeLoginContext(loginContext, httpRequest, httpResponse);
    329             loginHandler.login(httpRequest, httpResponse);
    330         } catch (AuthenticationException e) {
    331             loginContext.setAuthenticationFailure(e);
    332             returnToProfileHandler(loginContext, httpRequest, httpResponse);
    333         }
    334     }
    335
    336     /**
    337      * Determines which configured login handlers will support the requested authentication methods.
    338      *
    339      * @param loginContext current login context
    340      *
    341      * @return login methods that may be used to authenticate the user
    342      *
    343      * @throws AuthenticationException thrown if no login handler meets the given requirements
    344      */
    345     protected Map<String, LoginHandler> determinePossibleLoginHandlers(LoginContext loginContext)
    346             throws AuthenticationException {
    347         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(handlerManager
    348                 .getLoginHandlers());
    349         LOG.trace("Supported login handlers: {}", supportedLoginHandlers);
    350         LOG.trace("Requested authentication methods: {}", loginContext.getRequestedAuthenticationMethods());
    351
    352         // If no preferences Authn method preference is given, then we're free to use any
    353         if (loginContext.getRequestedAuthenticationMethods().isEmpty()) {
    354             LOG.trace("No preference given for authentication methods");
    355             return supportedLoginHandlers;
    356         }
    357
    358         // Otherwise we need to filter all the mechanism supported by the IdP so that only the request types are left
    359         // Previous session handler is a special case, we always to keep that around if it's configured
    360         Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet().iterator();
    361         Entry<String, LoginHandler> supportedLoginHandler;
    362         while (supportedLoginHandlerItr.hasNext()) {
    363             supportedLoginHandler = supportedLoginHandlerItr.next();
    364             if (!supportedLoginHandler.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
    365                     && !loginContext.getRequestedAuthenticationMethods().contains(supportedLoginHandler.getKey())) {
    366                 supportedLoginHandlerItr.remove();
    367                 continue;
    368             }
    369         }
    370
    371         if (supportedLoginHandlers.isEmpty()) {
    372             LOG.error("No authentication method, requested by the service provider, is supported");
    373             throw new AuthenticationException(
    374                     "No authentication method, requested by the service provider, is supported");
    375         }
    376
    377         return supportedLoginHandlers;
    378     }
    379
    380     /**
    381      * Filters out any login handler based on the requirement for forced authentication.
    382      *
    383      * During forced authentication any handler that has not previously been used to authenticate the user or any
    384      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
    385      *
    386      * @param idpSession user's current IdP session
    387      * @param loginContext current login context
    388      * @param loginHandlers login handlers to filter
    389      *
    390      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
    391      */
    392     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
    393             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
    394         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
    395
    396         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
    397         if (idpSession != null) {
    398             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
    399         }
    400
    401         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    402
    403         LoginHandler loginHandler;
    404         for (AuthenticationMethodInformation activeMethod : activeMethods) {
    405             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
    406             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
    407                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
    408                     loginHandlers.remove(handlerSupportedMethods);
    409                 }
    410             }
    411         }
    412
    413         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
    414                 loginHandlers);
    415
    416         if (loginHandlers.isEmpty()) {
    417             LOG.info("Force authentication requested but no login handlers available to support it");
    418             throw new ForceAuthenticationException();
    419         }
    420     }
    421
    422     /**
    423      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
    424      * authentication is required.
    425      *
    426      * @param idpSession user's current IdP session
    427      * @param loginContext current login context
    428      * @param loginHandlers login handlers to filter
    429      *
    430      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
    431      */
    432     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
    433             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
    434         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
    435
    436         if (idpSession == null) {
    437             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
    438         }
    439
    440         LoginHandler loginHandler;
    441         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
    442         while (authnMethodItr.hasNext()) {
    443             loginHandler = authnMethodItr.next().getValue();
    444             if (!loginHandler.supportsPassive()) {
    445                 authnMethodItr.remove();
    446             }
    447         }
    448
    449         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
    450                 loginHandlers);
    451
    452         if (loginHandlers.isEmpty()) {
    453             LOG.error("Passive authentication required but no login handlers available to support it");
    454             throw new PassiveAuthenticationException();
    455         }
    456     }
    457
    458     /**
    459      * Stores the login context in the storage service. The key for the stored login context is then bound to an HTTP
    460      * request attribute and set a cookie.
    461      *
    462      * @param loginContext login context to store
    463      * @param httpRequest current HTTP request
    464      * @param httpResponse current HTTP response
    465      */
    466     protected void storeLoginContext(LoginContext loginContext, HttpServletRequest httpRequest,
    467             HttpServletResponse httpResponse) {
    468         String contextId = idGen.generateIdentifier();
    469
    470         storageService.put(loginContextPartitionName, contextId, new LoginContextEntry(loginContext,
    471                 loginContextEntryLifetime));
    472
    473         httpRequest.setAttribute(LOGIN_CONTEXT_KEY_NAME, contextId);
    474
    475         Cookie cookie = new Cookie(LOGIN_CONTEXT_KEY_NAME, contextId);
    476         String contextPath = httpRequest.getContextPath();
    477         if (DatatypeHelper.isEmpty(contextPath)) {
    478             cookie.setPath("/");
    479         } else {
    480             cookie.setPath(contextPath);
    481         }
    482         cookie.setSecure(httpRequest.isSecure());
    483         cookie.setMaxAge(-1);
    484         httpResponse.addCookie(cookie);
    485     }
    486
    487     /**
    488      * Completes the authentication process.
    489      *
    490      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
    491      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
    492      * recorded and finally control is returned back to the profile handler.
    493      *
    494      * @param loginContext current login context
    495      * @param httpRequest current HTTP request
    496      * @param httpResponse current HTTP response
    497      */
    498     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
    499             HttpServletResponse httpResponse) {
    500         LOG.debug("Completing user authentication process");
    501
    502         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    503
    504         try {
    505             // Check to make sure the login handler did the right thing
    506             validateSuccessfulAuthentication(loginContext, httpRequest);
    507
    508             // We allow a login handler to override the authentication method in the
    509             // event that it supports multiple methods
    510             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    511                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
    512             if (actualAuthnMethod == null) {
    513                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
    514             }
    515
    516             // Get the Subject from the request. If force authentication was required then make sure the
    517             // Subject identifies the same user that authenticated before
    518             Subject subject = getLoginHandlerSubject(httpRequest);
    519             if (loginContext.isForceAuthRequired()) {
    520                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
    521             }
    522
    523             loginContext.setPrincipalAuthenticated(true);
    524             updateUserSession(loginContext, subject, actualAuthnMethod, httpRequest, httpResponse);
    525             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(), actualAuthnMethod);
    526         } catch (AuthenticationException e) {
    527             LOG.error("Authentication failed with the error:", e);
    528             loginContext.setPrincipalAuthenticated(false);
    529             loginContext.setAuthenticationFailure(e);
    530         }
    531
    532         returnToProfileHandler(loginContext, httpRequest, httpResponse);
    533     }
    534
    535     /**
    536      * Validates that the authentication was successfully performed by the login handler. An authentication is
    537      * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
    538      * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
    539      * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
    540      *
    541      * @param loginContext current login context
    542      * @param httpRequest current HTTP request
    543      *
    544      * @throws AuthenticationException thrown if the authentication was not successful
    545      */
    546     protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest)
    547             throws AuthenticationException {
    548         LOG.debug("Validating authentication was performed successfully");
    549
    550         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
    551                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
    552         if (errorMessage != null) {
    553             LOG.error("Error returned from login handler for authentication method {}:\n{}", loginContext
    554                     .getAttemptedAuthnMethod(), errorMessage);
    555             throw new AuthenticationException(errorMessage);
    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         LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
    655                 authenticationPrincipal.getName());
    656         LoginHandler loginHandler = handlerManager.getLoginHandlers().get(authenticationMethod);
    657         AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession
    658                 .getSubject(), authenticationPrincipal, authenticationMethod, new DateTime(), loginHandler
    659                 .getAuthenticationDuration());
    660
    661         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
    662         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
    663         sessionManager.indexSession(idpSession, authnMethodInfo.getAuthenticationPrincipal().getName());
    664
    665         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
    666                 authnMethodInfo);
    667         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
    668     }
    669
    670     /**
    671      * Merges the principals and public and private credentials from two subjects into a new subject.
    672      *
    673      * @param subject1 first subject to merge, may be null
    674      * @param subject2 second subject to merge, may be null
    675      *
    676      * @return subject containing the merged information
    677      */
    678     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
    679         if (subject1 == null) {
    680             return subject2;
    681         }
    682
    683         if (subject2 == null) {
    684             return subject1;
    685         }
    686
    687         if (subject1 == null && subject2 == null) {
    688             return new Subject();
    689         }
    690
    691         Set<Principal> principals = new HashSet<Principal>();
    692         principals.addAll(subject1.getPrincipals());
    693         principals.addAll(subject2.getPrincipals());
    694
    695         Set<Object> publicCredentials = new HashSet<Object>();
    696         publicCredentials.addAll(subject1.getPublicCredentials());
    697         publicCredentials.addAll(subject2.getPublicCredentials());
    698
    699         Set<Object> privateCredentials = new HashSet<Object>();
    700         privateCredentials.addAll(subject1.getPrivateCredentials());
    701         privateCredentials.addAll(subject2.getPrivateCredentials());
    702
    703         return new Subject(false, principals, publicCredentials, privateCredentials);
    704     }
    705
    706     /**
    707      * Adds an IdP session cookie to the outbound response.
    708      *
    709      * @param httpRequest current request
    710      * @param httpResponse current response
    711      * @param userSession user's session
    712      */
    713     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
    714             Session userSession) {
    715         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
    716
    717         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
    718         byte[] sessionId = userSession.getSessionID().getBytes();
    719
    720         String signature = null;
    721         try {
    722             MessageDigest digester = MessageDigest.getInstance("SHA");
    723             digester.update(userSession.getSessionSecret());
    724             digester.update(remoteAddress);
    725             digester.update(sessionId);
    726             signature = Base64.encodeBytes(digester.digest());
    727         } catch (GeneralSecurityException e) {
    728             LOG.error("Unable to compute signature over session cookie material", e);
    729         }
    730
    731         LOG.debug("Adding IdP session cookie to HTTP response");
    732         StringBuilder cookieValue = new StringBuilder();
    733         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
    734         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
    735         cookieValue.append(signature);
    736         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
    737
    738         String contextPath = httpRequest.getContextPath();
    739         if (DatatypeHelper.isEmpty(contextPath)) {
    740             sessionCookie.setPath("/");
    741         } else {
    742             sessionCookie.setPath(contextPath);
    743         }
    744
    745         sessionCookie.setSecure(httpRequest.isSecure());
    746         sessionCookie.setMaxAge(-1);
    747
    748         httpResponse.addCookie(sessionCookie);
    749     }
    750 }