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