View Javadoc

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