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