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