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