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