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