View Javadoc

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