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