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