View Javadoc

1   /*
2    * Copyright 2006 University Corporation for Advanced Internet Development, Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package edu.internet2.middleware.shibboleth.idp.authn;
18  
19  import java.io.IOException;
20  import java.security.GeneralSecurityException;
21  import java.security.MessageDigest;
22  import java.security.Principal;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  import java.util.Set;
32  
33  import javax.security.auth.Subject;
34  import javax.servlet.RequestDispatcher;
35  import javax.servlet.ServletConfig;
36  import javax.servlet.ServletContext;
37  import javax.servlet.ServletException;
38  import javax.servlet.http.Cookie;
39  import javax.servlet.http.HttpServlet;
40  import javax.servlet.http.HttpServletRequest;
41  import javax.servlet.http.HttpServletResponse;
42  
43  import org.joda.time.DateTime;
44  import org.opensaml.saml2.core.AuthnContext;
45  import org.opensaml.util.storage.StorageService;
46  import org.opensaml.ws.transport.http.HTTPTransportUtils;
47  import org.opensaml.xml.util.Base64;
48  import org.opensaml.xml.util.DatatypeHelper;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import edu.internet2.middleware.shibboleth.common.session.SessionManager;
53  import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
54  import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
55  import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
56  import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
57  import edu.internet2.middleware.shibboleth.idp.session.Session;
58  import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
59  import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
60  import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
61  
62  /** Manager responsible for handling authentication requests. */
63  public class AuthenticationEngine extends HttpServlet {
64  
65      /**
66       * Name of the Servlet config init parameter that indicates whether the public credentials of a {@link Subject} are
67       * retained after authentication.
68       */
69      public static final String RETAIN_PUBLIC_CREDENTIALS = "retainSubjectsPublicCredentials";
70  
71      /**
72       * Name of the Servlet config init parameter that indicates whether the private credentials of a {@link Subject} are
73       * retained after authentication.
74       */
75      public static final String RETAIN_PRIVATE_CREDENTIALS = "retainSubjectsPrivateCredentials";
76  
77      /** Name of the Servlet config init parameter that holds the partition name for login contexts. */
78      public static final String LOGIN_CONTEXT_PARTITION_NAME_INIT_PARAM_NAME = "loginContextPartitionName";
79  
80      /** Name of the Servlet config init parameter that holds lifetime of a login context in the storage service. */
81      public static final String LOGIN_CONTEXT_LIFETIME_INIT_PARAM_NAME = "loginContextEntryLifetime";
82  
83      /** Name of the IdP Cookie containing the IdP session ID. */
84      public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
85  
86      /** Name of the key under which to bind the storage service key for a login context. */
87      public static final String LOGIN_CONTEXT_KEY_NAME = "_idp_authn_lc_key";
88  
89      /** Serial version UID. */
90      private static final long serialVersionUID = -8479060989001890156L;
91  
92      /** Class logger. */
93      private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
94  
95      // TODO remove once HttpServletHelper does redirects
96      private static ServletContext context;
97  
98      /** Storage service used to store {@link LoginContext}s while authentication is in progress. */
99      private static StorageService<String, LoginContextEntry> storageService;
100 
101     /** Whether the public credentials of a {@link Subject} are retained after authentication. */
102     private boolean retainSubjectsPublicCredentials;
103 
104     /** Whether the private credentials of a {@link Subject} are retained after authentication. */
105     private boolean retainSubjectsPrivateCredentials;
106 
107     /** Profile handler manager. */
108     private IdPProfileHandlerManager handlerManager;
109 
110     /** Session manager. */
111     private SessionManager<Session> sessionManager;
112 
113     /** {@inheritDoc} */
114     public void init(ServletConfig config) throws ServletException {
115         super.init(config);
116 
117         String retain = DatatypeHelper.safeTrimOrNullString(config.getInitParameter(RETAIN_PRIVATE_CREDENTIALS));
118         if (retain != null) {
119             retainSubjectsPrivateCredentials = Boolean.parseBoolean(retain);
120         } else {
121             retainSubjectsPrivateCredentials = false;
122         }
123 
124         retain = DatatypeHelper.safeTrimOrNullString(config.getInitParameter(RETAIN_PUBLIC_CREDENTIALS));
125         if (retain != null) {
126             retainSubjectsPublicCredentials = Boolean.parseBoolean(retain);
127         } else {
128             retainSubjectsPublicCredentials = false;
129         }
130         context = config.getServletContext();
131         handlerManager = HttpServletHelper.getProfileHandlerManager(context);
132         sessionManager = HttpServletHelper.getSessionManager(context);
133         storageService = (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(context);
134     }
135 
136     /**
137      * Returns control back to the authentication engine.
138      * 
139      * @param httpRequest current HTTP request
140      * @param httpResponse current HTTP response
141      */
142     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
143         LOG.debug("Returning control to authentication engine");
144         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
145         if (loginContext == null) {
146             LOG.warn("No login context available, unable to return to authentication engine");
147             forwardRequest("/error.jsp", httpRequest, httpResponse);
148         } else {
149             forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
150         }
151     }
152 
153     /**
154      * Returns control back to the profile handler that invoked the authentication engine.
155      * 
156      * @param httpRequest current HTTP request
157      * @param httpResponse current HTTP response
158      */
159     public static void returnToProfileHandler(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
160         LOG.debug("Returning control to profile handler");
161         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
162         if (loginContext == null) {
163             LOG.warn("No login context available, unable to return to profile handler");
164             forwardRequest("/error.jsp", httpRequest, httpResponse);
165         }
166 
167         String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, loginContext.getProfileHandlerURL())
168                 .buildURL();
169         LOG.debug("Redirecting user to profile handler at {}", profileUrl);
170         try {
171             httpResponse.sendRedirect(profileUrl);
172         } catch (IOException e) {
173             LOG.warn("Error sending user back to profile handler at " + profileUrl, e);
174         }
175     }
176 
177     /**
178      * Forwards a request to the given path.
179      * 
180      * @param forwardPath path to forward the request to
181      * @param httpRequest current HTTP request
182      * @param httpResponse current HTTP response
183      */
184     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
185             HttpServletResponse httpResponse) {
186         try {
187             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
188             dispatcher.forward(httpRequest, httpResponse);
189             return;
190         } catch (IOException e) {
191             LOG.error("Unable to return control back to authentication engine", e);
192         } catch (ServletException e) {
193             LOG.error("Unable to return control back to authentication engine", e);
194         }
195     }
196 
197     /** {@inheritDoc} */
198     @SuppressWarnings("unchecked")
199     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
200             IOException {
201         LOG.debug("Processing incoming request");
202 
203         if (httpResponse.isCommitted()) {
204             LOG.error("HTTP Response already committed");
205         }
206 
207         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, getServletContext(), httpRequest);
208         if (loginContext == null) {
209             LOG.warn("No login context available, unable to proceed with authentication");
210             forwardRequest("/error.jsp", httpRequest, httpResponse);
211             return;
212         }
213 
214         if (!loginContext.getAuthenticationAttempted()) {
215             startUserAuthentication(loginContext, httpRequest, httpResponse);
216         } else {
217             completeAuthentication(loginContext, httpRequest, httpResponse);
218         }
219     }
220 
221     /**
222      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
223      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
224      * depending on whether passive authentication is required.
225      * 
226      * @param loginContext current login context
227      * @param httpRequest current HTTP request
228      * @param httpResponse current HTTP response
229      */
230     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
231             HttpServletResponse httpResponse) {
232         LOG.debug("Beginning user authentication process.");
233         try {
234             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
235             if (idpSession != null) {
236                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
237             }
238 
239             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
240 
241             // Filter out possible candidate login handlers by forced and passive authentication requirements
242             if (loginContext.isForceAuthRequired()) {
243                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
244             }
245 
246             if (loginContext.isPassiveAuthRequired()) {
247                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
248             }
249 
250             LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
251             loginContext.setAuthenticationAttempted();
252             loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
253 
254             // Send the request to the login handler
255             HttpServletHelper.bindLoginContext(loginContext, storageService, getServletContext(), httpRequest,
256                     httpResponse);
257             loginHandler.login(httpRequest, httpResponse);
258         } catch (AuthenticationException e) {
259             loginContext.setAuthenticationFailure(e);
260             returnToProfileHandler(httpRequest, httpResponse);
261         }
262     }
263 
264     /**
265      * Determines which configured login handlers will support the requested authentication methods.
266      * 
267      * @param loginContext current login context
268      * @param idpSession current user's session, or null if they don't have one
269      * 
270      * @return login methods that may be used to authenticate the user
271      * 
272      * @throws AuthenticationException thrown if no login handler meets the given requirements
273      */
274     protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
275             throws AuthenticationException {
276         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(
277                 handlerManager.getLoginHandlers());
278         LOG.debug("Filtering configured LoginHandlers: {}", supportedLoginHandlers);
279 
280         // First, if the service provider requested a particular authentication method, filter out everything but
281         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
282         if (requestedMethods != null && !requestedMethods.isEmpty()) {
283             LOG.debug("Filtering possible login handlers by requested authentication methods: {}", requestedMethods);
284             Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet()
285                     .iterator();
286             Entry<String, LoginHandler> supportedLoginHandlerEntry;
287             while (supportedLoginHandlerItr.hasNext()) {
288                 supportedLoginHandlerEntry = supportedLoginHandlerItr.next();
289                 if (!supportedLoginHandlerEntry.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
290                         && !requestedMethods.contains(supportedLoginHandlerEntry.getKey())) {
291                     LOG.debug(
292                             "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
293                             supportedLoginHandlerEntry.getKey());
294                     supportedLoginHandlerItr.remove();
295                 }
296             }
297         }
298 
299         // Next, determine, if present, if the previous session handler can be used
300         filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
301 
302         if (supportedLoginHandlers.isEmpty()) {
303             LOG.warn("No authentication method, requested by the service provider, is supported");
304             throw new AuthenticationException(
305                     "No authentication method, requested by the service provider, is supported");
306         }
307 
308         return supportedLoginHandlers;
309     }
310 
311     /**
312      * Filters out the previous session login handler if there is no existing IdP session, no active authentication
313      * methods, or if at least one of the active authentication methods do not match the requested authentication
314      * methods.
315      * 
316      * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
317      * @param idpSession current IdP session, may be null if no session currently exists
318      * @param loginContext current login context, never null
319      */
320     protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
321             Session idpSession, LoginContext loginContext) {
322         if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
323             return;
324         }
325 
326         if (idpSession == null) {
327             LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
328             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
329             return;
330         }
331         Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
332                 .values();
333 
334         Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
335         while (methodItr.hasNext()) {
336             AuthenticationMethodInformation info = methodItr.next();
337             if (info.isExpired()) {
338                 methodItr.remove();
339             }
340         }
341         if (currentAuthnMethods.isEmpty()) {
342             LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
343             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
344             return;
345         }
346 
347         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
348         if (requestedMethods != null && !requestedMethods.isEmpty()) {
349             boolean retainPreviousSession = false;
350             for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
351                 if (loginContext.getRequestedAuthenticationMethods().contains(
352                         currentAuthnMethod.getAuthenticationMethod())) {
353                     retainPreviousSession = true;
354                     break;
355                 }
356             }
357 
358             if (!retainPreviousSession) {
359                 LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
360                 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
361                 return;
362             }
363         }
364     }
365 
366     /**
367      * Filters out any login handler based on the requirement for forced authentication.
368      * 
369      * During forced authentication any handler that has not previously been used to authenticate the user or any
370      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
371      * 
372      * @param idpSession user's current IdP session
373      * @param loginContext current login context
374      * @param loginHandlers login handlers to filter
375      * 
376      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
377      */
378     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
379             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
380         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
381 
382         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
383         if (idpSession != null) {
384             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
385         }
386 
387         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
388 
389         LoginHandler loginHandler;
390         for (AuthenticationMethodInformation activeMethod : activeMethods) {
391             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
392             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
393                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
394                     LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
395                             .getClass().getName());
396                     loginHandlers.remove(handlerSupportedMethods);
397                 }
398             }
399         }
400 
401         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
402                 loginHandlers);
403 
404         if (loginHandlers.isEmpty()) {
405             LOG.info("Force authentication requested but no login handlers available to support it");
406             throw new ForceAuthenticationException();
407         }
408     }
409 
410     /**
411      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
412      * authentication is required.
413      * 
414      * @param idpSession user's current IdP session
415      * @param loginContext current login context
416      * @param loginHandlers login handlers to filter
417      * 
418      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
419      */
420     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
421             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
422         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
423 
424         if (idpSession == null) {
425             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
426         }
427 
428         LoginHandler loginHandler;
429         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
430         while (authnMethodItr.hasNext()) {
431             loginHandler = authnMethodItr.next().getValue();
432             if (!loginHandler.supportsPassive()) {
433                 authnMethodItr.remove();
434             }
435         }
436 
437         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
438                 loginHandlers);
439 
440         if (loginHandlers.isEmpty()) {
441             LOG.warn("Passive authentication required but no login handlers available to support it");
442             throw new PassiveAuthenticationException();
443         }
444     }
445 
446     /**
447      * Selects a login handler from a list of possible login handlers that could be used for the request.
448      * 
449      * @param possibleLoginHandlers list of possible login handlers that could be used for the request
450      * @param loginContext current login context
451      * @param idpSession current IdP session, if one exists
452      * 
453      * @return the login handler to use for this request
454      * 
455      * @throws AuthenticationException thrown if no handler can be used for this request
456      */
457     protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
458             LoginContext loginContext, Session idpSession) throws AuthenticationException {
459         LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
460         LoginHandler loginHandler;
461         if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
462             LOG.debug("Authenticating user with previous session LoginHandler");
463             loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
464 
465             for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
466                 if (authnMethod.isExpired()) {
467                     continue;
468                 }
469 
470                 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
471                         || loginContext.getRequestedAuthenticationMethods().contains(
472                                 authnMethod.getAuthenticationMethod())) {
473                     LOG.debug("Basing previous session authentication on active authentication method {}",
474                             authnMethod.getAuthenticationMethod());
475                     loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
476                     loginContext.setAuthenticationMethodInformation(authnMethod);
477                     return loginHandler;
478                 }
479             }
480         }
481 
482         if (loginContext.getDefaultAuthenticationMethod() != null
483                 && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
484             loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
485             loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
486         } else {
487             Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
488             loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
489             loginHandler = chosenLoginHandler.getValue();
490         }
491 
492         LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
493         return loginHandler;
494     }
495 
496     /**
497      * Completes the authentication process.
498      * 
499      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
500      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
501      * recorded and finally control is returned back to the profile handler.
502      * 
503      * @param loginContext current login context
504      * @param httpRequest current HTTP request
505      * @param httpResponse current HTTP response
506      */
507     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
508             HttpServletResponse httpResponse) {
509         LOG.debug("Completing user authentication process");
510 
511         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
512 
513         try {
514             // We allow a login handler to override the authentication method in the
515             // event that it supports multiple methods
516             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
517                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
518             if (actualAuthnMethod != null) {
519                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
520                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
521                     String msg = "Relying patry required an authentication method of "
522                             + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
523                             + actualAuthnMethod;
524                     LOG.error(msg);
525                     throw new AuthenticationException(msg);
526                 }
527             } else {
528                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
529             }
530 
531             // Check to make sure the login handler did the right thing
532             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
533 
534             // Check for an overridden authn instant.
535             DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
536 
537             // Get the Subject from the request. If force authentication was required then make sure the
538             // Subject identifies the same user that authenticated before
539             Subject subject = getLoginHandlerSubject(httpRequest);
540             if (loginContext.isForceAuthRequired()) {
541                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
542 
543                 // Reset the authn instant.
544                 if (actualAuthnInstant == null) {
545                     actualAuthnInstant = new DateTime();
546                 }
547             }
548 
549             loginContext.setPrincipalAuthenticated(true);
550             updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
551             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
552                     loginContext.getAuthenticationMethod());
553         } catch (AuthenticationException e) {
554             LOG.error("Authentication failed with the error:", e);
555             loginContext.setPrincipalAuthenticated(false);
556             loginContext.setAuthenticationFailure(e);
557         }
558 
559         returnToProfileHandler(httpRequest, httpResponse);
560     }
561 
562     /**
563      * Validates that the authentication was successfully performed by the login handler. An authentication is
564      * considered successful if no error is bound to the request attribute {@link LoginHandler#AUTHENTICATION_ERROR_KEY}
565      * and there is a value for at least one of the following request attributes: {@link LoginHandler#SUBJECT_KEY},
566      * {@link LoginHandler#PRINCIPAL_KEY}, or {@link LoginHandler#PRINCIPAL_NAME_KEY}.
567      * 
568      * @param loginContext current login context
569      * @param httpRequest current HTTP request
570      * @param authenticationMethod the authentication method used to authenticate the user
571      * 
572      * @throws AuthenticationException thrown if the authentication was not successful
573      */
574     protected void validateSuccessfulAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
575             String authenticationMethod) throws AuthenticationException {
576         LOG.debug("Validating authentication was performed successfully");
577 
578         if (authenticationMethod == null) {
579             LOG.error("No authentication method reported by login handler.");
580             throw new AuthenticationException("No authentication method reported by login handler.");
581         }
582 
583         String errorMessage = DatatypeHelper.safeTrimOrNullString((String) httpRequest
584                 .getAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY));
585         if (errorMessage != null) {
586             LOG.error("Error returned from login handler for authentication method {}:\n{}",
587                     loginContext.getAttemptedAuthnMethod(), errorMessage);
588             throw new AuthenticationException(errorMessage);
589         }
590 
591         AuthenticationException authnException = (AuthenticationException) httpRequest
592                 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
593         if (authnException != null) {
594             throw authnException;
595         }
596 
597         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
598         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
599         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
600                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
601 
602         if (subject == null && principal == null && principalName == null) {
603             LOG.error("No user identified by login handler.");
604             throw new AuthenticationException("No user identified by login handler.");
605         }
606     }
607 
608     /**
609      * Gets the subject from the request coming back from the login handler.
610      * 
611      * @param httpRequest request coming back from the login handler
612      * 
613      * @return the {@link Subject} created from the request
614      * 
615      * @throws AuthenticationException thrown if no subject can be retrieved from the request
616      */
617     protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
618         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
619         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
620         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
621                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
622 
623         if (subject == null && (principal != null || principalName != null)) {
624             subject = new Subject();
625             if (principal == null) {
626                 principal = new UsernamePrincipal(principalName);
627             }
628             subject.getPrincipals().add(principal);
629         }
630 
631         return subject;
632     }
633 
634     /**
635      * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
636      * principal name that is equal to the principal name associated with the authentication method. If this is the
637      * first time the subject has authenticated with this method than this check always passes.
638      * 
639      * @param idpSession user's IdP session
640      * @param authnMethod method used to authenticate the user
641      * @param subject subject that was authenticated
642      * 
643      * @throws AuthenticationException thrown if this check fails
644      */
645     protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
646             throws AuthenticationException {
647         if (idpSession != null) {
648             AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
649             if (authnMethodInfo != null) {
650                 boolean princpalMatch = false;
651                 for (Principal princpal : subject.getPrincipals()) {
652                     if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
653                         princpalMatch = true;
654                         break;
655                     }
656                 }
657 
658                 if (!princpalMatch) {
659                     throw new ForceAuthenticationException(
660                             "Authenticated principal does not match previously authenticated principal");
661                 }
662             }
663         }
664     }
665 
666     /**
667      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
668      * created.
669      * 
670      * @param loginContext current login context
671      * @param authenticationSubject subject created from the authentication method
672      * @param authenticationMethod the method used to authenticate the subject
673      * @param authenticationInstant the time of authentication
674      * @param httpRequest current HTTP request
675      * @param httpResponse current HTTP response
676      */
677     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
678             String authenticationMethod, DateTime authenticationInstant, HttpServletRequest httpRequest,
679             HttpServletResponse httpResponse) {
680         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
681         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
682 
683         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
684         if (idpSession == null) {
685             LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
686             idpSession = (Session) sessionManager.createSession();
687             loginContext.setSessionID(idpSession.getSessionID());
688             addSessionCookie(httpRequest, httpResponse, idpSession);
689         }
690 
691         // Merge the information in the current session subject with the information from the
692         // login handler subject
693         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
694 
695         // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
696         // if not record the new information
697         AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
698                 authenticationMethod);
699         if (authnMethodInfo == null || authenticationInstant != null) {
700             LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
701                     authenticationPrincipal.getName());
702             LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
703             DateTime authnInstant = authenticationInstant;
704             if (authnInstant == null) {
705                 authnInstant = new DateTime();
706             }
707             authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession.getSubject(), authenticationPrincipal,
708                     authenticationMethod, authnInstant, loginHandler.getAuthenticationDuration());
709         }
710 
711         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
712         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
713         sessionManager.indexSession(idpSession, idpSession.getPrincipalName());
714 
715         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
716                 authnMethodInfo);
717         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
718     }
719 
720     /**
721      * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
722      * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
723      * the private credentials from both subjects, if not the new subject will not contain private credentials. If
724      * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
725      * from both subjects, if not the new subject will not contain public credentials.
726      * 
727      * @param subject1 first subject to merge, may be null
728      * @param subject2 second subject to merge, may be null
729      * 
730      * @return subject containing the merged information
731      */
732     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
733         if (subject1 == null && subject2 == null) {
734             return new Subject();
735         }
736 
737         if (subject1 == null) {
738             return subject2;
739         }
740 
741         if (subject2 == null) {
742             return subject1;
743         }
744 
745         Set<Principal> principals = new HashSet<Principal>(3);
746         principals.addAll(subject1.getPrincipals());
747         principals.addAll(subject2.getPrincipals());
748 
749         Set<Object> publicCredentials = new HashSet<Object>(3);
750         if (retainSubjectsPublicCredentials) {
751             LOG.debug("Merging in subjects public credentials");
752             publicCredentials.addAll(subject1.getPublicCredentials());
753             publicCredentials.addAll(subject2.getPublicCredentials());
754         }
755 
756         Set<Object> privateCredentials = new HashSet<Object>(3);
757         if (retainSubjectsPrivateCredentials) {
758             LOG.debug("Merging in subjects private credentials");
759             privateCredentials.addAll(subject1.getPrivateCredentials());
760             privateCredentials.addAll(subject2.getPrivateCredentials());
761         }
762 
763         return new Subject(false, principals, publicCredentials, privateCredentials);
764     }
765 
766     /**
767      * Adds an IdP session cookie to the outbound response.
768      * 
769      * @param httpRequest current request
770      * @param httpResponse current response
771      * @param userSession user's session
772      */
773     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
774             Session userSession) {
775         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
776 
777         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
778         byte[] sessionId = userSession.getSessionID().getBytes();
779 
780         String signature = null;
781         try {
782             MessageDigest digester = MessageDigest.getInstance("SHA");
783             digester.update(userSession.getSessionSecret());
784             digester.update(remoteAddress);
785             digester.update(sessionId);
786             signature = Base64.encodeBytes(digester.digest());
787         } catch (GeneralSecurityException e) {
788             LOG.error("Unable to compute signature over session cookie material", e);
789         }
790 
791         LOG.debug("Adding IdP session cookie to HTTP response");
792         StringBuilder cookieValue = new StringBuilder();
793         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
794         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
795         cookieValue.append(signature);
796 
797         String cookieDomain = HttpServletHelper.getCookieDomain(context);
798 
799         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
800         sessionCookie.setVersion(1);
801         if (cookieDomain != null) {
802             sessionCookie.setDomain(cookieDomain);
803         }
804         sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
805         sessionCookie.setSecure(httpRequest.isSecure());
806         httpResponse.addCookie(sessionCookie);
807     }
808 }