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