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