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