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