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