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