Merge remote branch 'tags/2.3.4'
[java-idp.git] / src / main / java / edu / internet2 / middleware / shibboleth / idp / authn / AuthenticationEngine.java
1 /*
2  * Licensed to the University Corporation for Advanced Internet Development, 
3  * Inc. (UCAID) under one or more contributor license agreements.  See the 
4  * NOTICE file distributed with this work for additional information regarding
5  * copyright ownership. The UCAID licenses this file to You under the Apache 
6  * License, Version 2.0 (the "License"); you may not use this file except in 
7  * compliance with the License.  You may obtain a copy of the License at
8  *
9  *    http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package edu.internet2.middleware.shibboleth.idp.authn;
19
20 import java.io.IOException;
21 import java.security.GeneralSecurityException;
22 import java.security.MessageDigest;
23 import java.security.Principal;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Map.Entry;
32 import java.util.Set;
33
34 import javax.security.auth.Subject;
35 import javax.servlet.RequestDispatcher;
36 import javax.servlet.ServletConfig;
37 import javax.servlet.ServletContext;
38 import javax.servlet.ServletException;
39 import javax.servlet.http.Cookie;
40 import javax.servlet.http.HttpServlet;
41 import javax.servlet.http.HttpServletRequest;
42 import javax.servlet.http.HttpServletResponse;
43
44 import org.joda.time.DateTime;
45 import org.opensaml.saml2.core.AuthnContext;
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         }
152     }
153
154     /**
155      * Returns control back to the profile handler that invoked the authentication engine.
156      * 
157      * @param httpRequest current HTTP request
158      * @param httpResponse current HTTP response
159      */
160     public static void returnToProfileHandler(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
161         LOG.debug("Returning control to profile handler");
162         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, context, httpRequest);
163         if (loginContext == null) {
164             LOG.warn("No login context available, unable to return to profile handler");
165             forwardRequest("/error.jsp", httpRequest, httpResponse);
166             return;
167         }
168         
169         if (loginContext.getProfileHandlerURL() == null) {
170             LOG.warn("Login context did not contain a profile handler path, unable to return to profile handler");
171             forwardRequest("/error.jsp", httpRequest, httpResponse);
172             return;
173         }
174
175         String profileUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, loginContext.getProfileHandlerURL())
176                 .buildURL();
177         LOG.debug("Redirecting user to profile handler at {}", profileUrl);
178         try {
179             httpResponse.sendRedirect(profileUrl);
180         } catch (IOException e) {
181             LOG.warn("Error sending user back to profile handler at " + profileUrl, e);
182         }
183     }
184
185     /**
186      * Forwards a request to the given path.
187      * 
188      * @param forwardPath path to forward the request to
189      * @param httpRequest current HTTP request
190      * @param httpResponse current HTTP response
191      */
192     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
193             HttpServletResponse httpResponse) {
194         try {
195             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
196             dispatcher.forward(httpRequest, httpResponse);
197             return;
198         } catch (IOException e) {
199             LOG.error("Unable to return control back to authentication engine", e);
200         } catch (ServletException e) {
201             LOG.error("Unable to return control back to authentication engine", e);
202         }
203     }
204
205     /** {@inheritDoc} */
206     @SuppressWarnings("unchecked")
207     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
208             IOException {
209         LOG.debug("Processing incoming request");
210
211         if (httpResponse.isCommitted()) {
212             LOG.error("HTTP Response already committed");
213         }
214
215         LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, getServletContext(), httpRequest);
216         if (loginContext == null) {
217             LOG.warn("No login context available, unable to proceed with authentication");
218             forwardRequest("/error.jsp", httpRequest, httpResponse);
219             return;
220         }
221
222         if (!loginContext.getAuthenticationAttempted()) {
223             startUserAuthentication(loginContext, httpRequest, httpResponse);
224         } else {
225             completeAuthentication(loginContext, httpRequest, httpResponse);
226         }
227     }
228
229     /**
230      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
231      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
232      * depending on whether passive authentication is required.
233      * 
234      * @param loginContext current login context
235      * @param httpRequest current HTTP request
236      * @param httpResponse current HTTP response
237      */
238     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
239             HttpServletResponse httpResponse) {
240         LOG.debug("Beginning user authentication process.");
241         try {
242             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
243             if (idpSession != null) {
244                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
245             }
246
247             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(idpSession, loginContext);
248
249             // Filter out possible candidate login handlers by forced and passive authentication requirements
250             if (loginContext.isForceAuthRequired()) {
251                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
252             }
253
254             if (loginContext.isPassiveAuthRequired()) {
255                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
256             }
257
258             LoginHandler loginHandler = selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
259             loginContext.setAuthenticationAttempted();
260             loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
261
262             // Send the request to the login handler
263             HttpServletHelper.bindLoginContext(loginContext, storageService, getServletContext(), httpRequest,
264                     httpResponse);
265             loginHandler.login(httpRequest, httpResponse);
266         } catch (AuthenticationException e) {
267             loginContext.setAuthenticationFailure(e);
268             returnToProfileHandler(httpRequest, httpResponse);
269         }
270     }
271
272     /**
273      * Determines which configured login handlers will support the requested authentication methods.
274      * 
275      * @param loginContext current login context
276      * @param idpSession current user's session, or null if they don't have one
277      * 
278      * @return login methods that may be used to authenticate the user
279      * 
280      * @throws AuthenticationException thrown if no login handler meets the given requirements
281      */
282     protected Map<String, LoginHandler> determinePossibleLoginHandlers(Session idpSession, LoginContext loginContext)
283             throws AuthenticationException {
284         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(
285                 handlerManager.getLoginHandlers());
286         LOG.debug("Filtering configured LoginHandlers: {}", supportedLoginHandlers);
287
288         // First, if the service provider requested a particular authentication method, filter out everything but
289         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
290         if (requestedMethods != null && !requestedMethods.isEmpty()) {
291             LOG.debug("Filtering possible login handlers by requested authentication methods: {}", requestedMethods);
292             Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet()
293                     .iterator();
294             Entry<String, LoginHandler> supportedLoginHandlerEntry;
295             while (supportedLoginHandlerItr.hasNext()) {
296                 supportedLoginHandlerEntry = supportedLoginHandlerItr.next();
297                 if (!supportedLoginHandlerEntry.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
298                         && !requestedMethods.contains(supportedLoginHandlerEntry.getKey())) {
299                     LOG.debug(
300                             "Filtering out login handler for authentication {}, it does not provide a requested authentication method",
301                             supportedLoginHandlerEntry.getKey());
302                     supportedLoginHandlerItr.remove();
303                 }
304             }
305         }
306
307         // Next, determine, if present, if the previous session handler can be used
308         filterPreviousSessionLoginHandler(supportedLoginHandlers, idpSession, loginContext);
309
310         if (supportedLoginHandlers.isEmpty()) {
311             LOG.warn("No authentication method, requested by the service provider, is supported");
312             throw new AuthenticationException(
313                     "No authentication method, requested by the service provider, is supported");
314         }
315
316         return supportedLoginHandlers;
317     }
318
319     /**
320      * Filters out the previous session login handler if there is no existing IdP session, no active authentication
321      * methods, or if at least one of the active authentication methods do not match the requested authentication
322      * methods.
323      * 
324      * @param supportedLoginHandlers login handlers supported by the authentication engine for this request, never null
325      * @param idpSession current IdP session, may be null if no session currently exists
326      * @param loginContext current login context, never null
327      */
328     protected void filterPreviousSessionLoginHandler(Map<String, LoginHandler> supportedLoginHandlers,
329             Session idpSession, LoginContext loginContext) {
330         if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
331             return;
332         }
333
334         if (idpSession == null) {
335             LOG.debug("Filtering out previous session login handler because there is no existing IdP session");
336             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
337             return;
338         }
339         Collection<AuthenticationMethodInformation> currentAuthnMethods = idpSession.getAuthenticationMethods()
340                 .values();
341
342         Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
343         while (methodItr.hasNext()) {
344             AuthenticationMethodInformation info = methodItr.next();
345             if (info.isExpired()) {
346                 methodItr.remove();
347             }
348         }
349         if (currentAuthnMethods.isEmpty()) {
350             LOG.debug("Filtering out previous session login handler because there are no active authentication methods");
351             supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
352             return;
353         }
354
355         List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
356         if (requestedMethods != null && !requestedMethods.isEmpty()) {
357             boolean retainPreviousSession = false;
358             for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
359                 if (loginContext.getRequestedAuthenticationMethods().contains(
360                         currentAuthnMethod.getAuthenticationMethod())) {
361                     retainPreviousSession = true;
362                     break;
363                 }
364             }
365
366             if (!retainPreviousSession) {
367                 LOG.debug("Filtering out previous session login handler, no active authentication methods match required methods");
368                 supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
369                 return;
370             }
371         }
372     }
373
374     /**
375      * Filters out any login handler based on the requirement for forced authentication.
376      * 
377      * During forced authentication any handler that has not previously been used to authenticate the user or any
378      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
379      * 
380      * @param idpSession user's current IdP session
381      * @param loginContext current login context
382      * @param loginHandlers login handlers to filter
383      * 
384      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
385      */
386     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
387             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
388         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
389
390         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
391         if (idpSession != null) {
392             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
393         }
394
395         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
396
397         LoginHandler loginHandler;
398         for (AuthenticationMethodInformation activeMethod : activeMethods) {
399             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
400             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
401                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
402                     LOG.debug("Removing LoginHandler {}, it does not support forced re-authentication", loginHandler
403                             .getClass().getName());
404                     loginHandlers.remove(handlerSupportedMethods);
405                 }
406             }
407         }
408
409         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
410                 loginHandlers);
411
412         if (loginHandlers.isEmpty()) {
413             LOG.info("Force authentication requested but no login handlers available to support it");
414             throw new ForceAuthenticationException();
415         }
416     }
417
418     /**
419      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
420      * authentication is required.
421      * 
422      * @param idpSession user's current IdP session
423      * @param loginContext current login context
424      * @param loginHandlers login handlers to filter
425      * 
426      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
427      */
428     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
429             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
430         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
431
432         if (idpSession == null) {
433             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
434         }
435
436         LoginHandler loginHandler;
437         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
438         while (authnMethodItr.hasNext()) {
439             loginHandler = authnMethodItr.next().getValue();
440             if (!loginHandler.supportsPassive()) {
441                 authnMethodItr.remove();
442             }
443         }
444
445         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
446                 loginHandlers);
447
448         if (loginHandlers.isEmpty()) {
449             LOG.warn("Passive authentication required but no login handlers available to support it");
450             throw new PassiveAuthenticationException();
451         }
452     }
453
454     /**
455      * Selects a login handler from a list of possible login handlers that could be used for the request.
456      * 
457      * @param possibleLoginHandlers list of possible login handlers that could be used for the request
458      * @param loginContext current login context
459      * @param idpSession current IdP session, if one exists
460      * 
461      * @return the login handler to use for this request
462      * 
463      * @throws AuthenticationException thrown if no handler can be used for this request
464      */
465     protected LoginHandler selectLoginHandler(Map<String, LoginHandler> possibleLoginHandlers,
466             LoginContext loginContext, Session idpSession) throws AuthenticationException {
467         LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
468         LoginHandler loginHandler;
469         if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
470             LOG.debug("Authenticating user with previous session LoginHandler");
471             loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
472
473             for (AuthenticationMethodInformation authnMethod : idpSession.getAuthenticationMethods().values()) {
474                 if (authnMethod.isExpired()) {
475                     continue;
476                 }
477
478                 if (loginContext.getRequestedAuthenticationMethods().isEmpty()
479                         || loginContext.getRequestedAuthenticationMethods().contains(
480                                 authnMethod.getAuthenticationMethod())) {
481                     LOG.debug("Basing previous session authentication on active authentication method {}",
482                             authnMethod.getAuthenticationMethod());
483                     loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
484                     loginContext.setAuthenticationMethodInformation(authnMethod);
485                     return loginHandler;
486                 }
487             }
488         }
489
490         if (loginContext.getDefaultAuthenticationMethod() != null
491                 && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
492             loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
493             loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
494         } else {
495             Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
496             loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
497             loginHandler = chosenLoginHandler.getValue();
498         }
499
500         LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
501         return loginHandler;
502     }
503
504     /**
505      * Completes the authentication process.
506      * 
507      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
508      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
509      * recorded and finally control is returned back to the profile handler.
510      * 
511      * @param loginContext current login context
512      * @param httpRequest current HTTP request
513      * @param httpResponse current HTTP response
514      */
515     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
516             HttpServletResponse httpResponse) {
517         LOG.debug("Completing user authentication process");
518
519         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
520
521         try {
522             // We allow a login handler to override the authentication method in the
523             // event that it supports multiple methods
524             String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
525                     .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
526             if (actualAuthnMethod != null) {
527                 if (!loginContext.getRequestedAuthenticationMethods().isEmpty()
528                         && !loginContext.getRequestedAuthenticationMethods().contains(actualAuthnMethod)) {
529                     String msg = "Relying patry required an authentication method of "
530                             + loginContext.getRequestedAuthenticationMethods() + " but the login handler performed "
531                             + actualAuthnMethod;
532                     LOG.error(msg);
533                     throw new AuthenticationException(msg);
534                 }
535             } else {
536                 actualAuthnMethod = loginContext.getAttemptedAuthnMethod();
537             }
538
539             // Check to make sure the login handler did the right thing
540             validateSuccessfulAuthentication(loginContext, httpRequest, actualAuthnMethod);
541             if(loginContext.getAuthenticationFailure() != null){
542                 returnToProfileHandler(httpRequest, httpResponse);
543             }
544
545             // Check for an overridden authn instant.
546             DateTime actualAuthnInstant = (DateTime) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_INSTANT_KEY);
547
548             // Get the Subject from the request. If force authentication was required then make sure the
549             // Subject identifies the same user that authenticated before
550             Subject subject = getLoginHandlerSubject(httpRequest);
551             if (loginContext.isForceAuthRequired()) {
552                 validateForcedReauthentication(idpSession, actualAuthnMethod, subject);
553
554                 // Reset the authn instant.
555                 if (actualAuthnInstant == null) {
556                     actualAuthnInstant = new DateTime();
557                 }
558             }
559
560             loginContext.setPrincipalAuthenticated(true);
561             updateUserSession(loginContext, subject, actualAuthnMethod, actualAuthnInstant, httpRequest, httpResponse);
562             LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(),
563                     loginContext.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.debug("Error returned from login handler for authentication method {}:\n{}",
598                     loginContext.getAttemptedAuthnMethod(), errorMessage);
599             loginContext.setAuthenticationFailure(new AuthenticationException(errorMessage));
600             loginContext.setPrincipalAuthenticated(false);
601             return;
602         }
603
604         AuthenticationException authnException = (AuthenticationException) httpRequest
605                 .getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
606         if (authnException != null) {
607             LOG.debug("Exception returned from login handler for authentication method {}:\n{}",
608                     loginContext.getAttemptedAuthnMethod(), authnException);
609             loginContext.setAuthenticationFailure(authnException);
610             loginContext.setPrincipalAuthenticated(false);
611             return;
612         }
613
614         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
615         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
616         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
617                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
618
619         if (subject == null && principal == null && principalName == null) {
620             LOG.error("No user identified by login handler.");
621             throw new AuthenticationException("No user identified by login handler.");
622         }
623     }
624
625     /**
626      * Gets the subject from the request coming back from the login handler.
627      * 
628      * @param httpRequest request coming back from the login handler
629      * 
630      * @return the {@link Subject} created from the request
631      * 
632      * @throws AuthenticationException thrown if no subject can be retrieved from the request
633      */
634     protected Subject getLoginHandlerSubject(HttpServletRequest httpRequest) throws AuthenticationException {
635         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
636         Principal principal = (Principal) httpRequest.getAttribute(LoginHandler.PRINCIPAL_KEY);
637         String principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
638                 .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
639
640         if (subject == null && (principal != null || principalName != null)) {
641             subject = new Subject();
642             if (principal == null) {
643                 principal = new UsernamePrincipal(principalName);
644             }
645             subject.getPrincipals().add(principal);
646         }
647
648         return subject;
649     }
650
651     /**
652      * If forced authentication was required this method checks to ensure that the re-authenticated subject contains a
653      * principal name that is equal to the principal name associated with the authentication method. If this is the
654      * first time the subject has authenticated with this method than this check always passes.
655      * 
656      * @param idpSession user's IdP session
657      * @param authnMethod method used to authenticate the user
658      * @param subject subject that was authenticated
659      * 
660      * @throws AuthenticationException thrown if this check fails
661      */
662     protected void validateForcedReauthentication(Session idpSession, String authnMethod, Subject subject)
663             throws AuthenticationException {
664         if (idpSession != null) {
665             AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(authnMethod);
666             if (authnMethodInfo != null) {
667                 boolean princpalMatch = false;
668                 for (Principal princpal : subject.getPrincipals()) {
669                     if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
670                         princpalMatch = true;
671                         break;
672                     }
673                 }
674
675                 if (!princpalMatch) {
676                     throw new ForceAuthenticationException(
677                             "Authenticated principal does not match previously authenticated principal");
678                 }
679             }
680         }
681     }
682
683     /**
684      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
685      * created.
686      * 
687      * @param loginContext current login context
688      * @param authenticationSubject subject created from the authentication method
689      * @param authenticationMethod the method used to authenticate the subject
690      * @param authenticationInstant the time of authentication
691      * @param httpRequest current HTTP request
692      * @param httpResponse current HTTP response
693      */
694     protected void updateUserSession(LoginContext loginContext, Subject authenticationSubject,
695             String authenticationMethod, DateTime authenticationInstant, HttpServletRequest httpRequest,
696             HttpServletResponse httpResponse) {
697         Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
698         LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());
699
700         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
701         if (idpSession == null) {
702             LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
703             idpSession = (Session) sessionManager.createSession();
704             loginContext.setSessionID(idpSession.getSessionID());
705             addSessionCookie(httpRequest, httpResponse, idpSession);
706         }
707
708         // Merge the information in the current session subject with the information from the
709         // login handler subject
710         idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));
711
712         // Check if an existing authentication method with no updated timestamp was used (i.e. SSO occurred);
713         // if not record the new information
714         AuthenticationMethodInformation authnMethodInfo = idpSession.getAuthenticationMethods().get(
715                 authenticationMethod);
716         if (authnMethodInfo == null || authenticationInstant != null) {
717             LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
718                     authenticationPrincipal.getName());
719             LoginHandler loginHandler = handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
720             DateTime authnInstant = authenticationInstant;
721             if (authnInstant == null) {
722                 authnInstant = new DateTime();
723             }
724             authnMethodInfo = new AuthenticationMethodInformationImpl(idpSession.getSubject(), authenticationPrincipal,
725                     authenticationMethod, authnInstant, loginHandler.getAuthenticationDuration());
726         }
727
728         loginContext.setAuthenticationMethodInformation(authnMethodInfo);
729         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
730         sessionManager.indexSession(idpSession, idpSession.getPrincipalName());
731
732         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
733                 authnMethodInfo);
734         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
735     }
736
737     /**
738      * Merges the two {@link Subject}s in to a new {@link Subject}. The new subjects contains all the {@link Principal}s
739      * from both subjects. If {@link #retainSubjectsPrivateCredentials} is true then the new subject will contain all
740      * the private credentials from both subjects, if not the new subject will not contain private credentials. If
741      * {@link #retainSubjectsPublicCredentials} is true then the new subject will contain all the public credentials
742      * from both subjects, if not the new subject will not contain public credentials.
743      * 
744      * @param subject1 first subject to merge, may be null
745      * @param subject2 second subject to merge, may be null
746      * 
747      * @return subject containing the merged information
748      */
749     protected Subject mergeSubjects(Subject subject1, Subject subject2) {
750         if (subject1 == null && subject2 == null) {
751             return new Subject();
752         }
753
754         if (subject1 == null) {
755             return subject2;
756         }
757
758         if (subject2 == null) {
759             return subject1;
760         }
761
762         Set<Principal> principals = new HashSet<Principal>(3);
763         principals.addAll(subject1.getPrincipals());
764         principals.addAll(subject2.getPrincipals());
765
766         Set<Object> publicCredentials = new HashSet<Object>(3);
767         if (retainSubjectsPublicCredentials) {
768             LOG.debug("Merging in subjects public credentials");
769             publicCredentials.addAll(subject1.getPublicCredentials());
770             publicCredentials.addAll(subject2.getPublicCredentials());
771         }
772
773         Set<Object> privateCredentials = new HashSet<Object>(3);
774         if (retainSubjectsPrivateCredentials) {
775             LOG.debug("Merging in subjects private credentials");
776             privateCredentials.addAll(subject1.getPrivateCredentials());
777             privateCredentials.addAll(subject2.getPrivateCredentials());
778         }
779
780         return new Subject(false, principals, publicCredentials, privateCredentials);
781     }
782
783     /**
784      * Adds an IdP session cookie to the outbound response.
785      * 
786      * @param httpRequest current request
787      * @param httpResponse current response
788      * @param userSession user's session
789      */
790     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
791             Session userSession) {
792         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
793
794         byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
795         byte[] sessionId = userSession.getSessionID().getBytes();
796
797         String signature = null;
798         try {
799             MessageDigest digester = MessageDigest.getInstance("SHA");
800             digester.update(userSession.getSessionSecret());
801             digester.update(remoteAddress);
802             digester.update(sessionId);
803             signature = Base64.encodeBytes(digester.digest());
804         } catch (GeneralSecurityException e) {
805             LOG.error("Unable to compute signature over session cookie material", e);
806         }
807
808         LOG.debug("Adding IdP session cookie to HTTP response");
809         StringBuilder cookieValue = new StringBuilder();
810         cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
811         cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
812         cookieValue.append(signature);
813
814         String cookieDomain = HttpServletHelper.getCookieDomain(context);
815
816         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
817         sessionCookie.setVersion(1);
818         if (cookieDomain != null) {
819             sessionCookie.setDomain(cookieDomain);
820         }
821         sessionCookie.setPath("".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
822         sessionCookie.setSecure(httpRequest.isSecure());
823         httpResponse.addCookie(sessionCookie);
824     }
825 }