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