Make the session cookie secure if the IdP is accepting authn requests over a secure...
[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.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.Map;
24 import java.util.Map.Entry;
25
26 import javax.security.auth.Subject;
27 import javax.servlet.RequestDispatcher;
28 import javax.servlet.ServletConfig;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.Cookie;
31 import javax.servlet.http.HttpServlet;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34 import javax.servlet.http.HttpSession;
35
36 import org.joda.time.DateTime;
37 import org.opensaml.saml2.core.AuthnContext;
38 import org.opensaml.xml.util.DatatypeHelper;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
43 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
44 import edu.internet2.middleware.shibboleth.idp.authn.provider.PreviousSessionLoginHandler;
45 import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
46 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
47 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
48 import edu.internet2.middleware.shibboleth.idp.session.Session;
49 import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
50 import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
51
52 /**
53  * Manager responsible for handling authentication requests.
54  */
55 public class AuthenticationEngine extends HttpServlet {
56
57     /** Name of the IdP Cookie containing the IdP session ID. */
58     public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
59
60     /** Serial version UID. */
61     private static final long serialVersionUID = 8494202791991613148L;
62
63     /** Class logger. */
64     private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
65
66     /** Profile handler manager. */
67     private IdPProfileHandlerManager handlerManager;
68
69     /** Session manager. */
70     private SessionManager<Session> sessionManager;
71
72     /** {@inheritDoc} */
73     public void init(ServletConfig config) throws ServletException {
74         super.init(config);
75
76         String handlerManagerId = config.getInitParameter("handlerManagerId");
77         if (DatatypeHelper.isEmpty(handlerManagerId)) {
78             handlerManagerId = "shibboleth.HandlerManager";
79         }
80         handlerManager = (IdPProfileHandlerManager) getServletContext().getAttribute(handlerManagerId);
81
82         String sessionManagerId = config.getInitParameter("sessionManagedId");
83         if (DatatypeHelper.isEmpty(sessionManagerId)) {
84             sessionManagerId = "shibboleth.SessionManager";
85         }
86
87         sessionManager = (SessionManager<Session>) getServletContext().getAttribute(sessionManagerId);
88     }
89
90     /**
91      * Returns control back to the authentication engine.
92      * 
93      * @param httpRequest current http request
94      * @param httpResponse current http response
95      */
96     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
97         LOG.debug("Returning control to authentication engine");
98         HttpSession httpSession = httpRequest.getSession();
99         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
100         if (loginContext == null) {
101             LOG.error("User HttpSession did not contain a login context.  Unable to return to authentication engine");
102             forwardRequest("/idp-error.jsp", httpRequest, httpResponse);
103         } else {
104             forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
105         }
106     }
107
108     /**
109      * Returns control back to the profile handler that invoked the authentication engine.
110      * 
111      * @param loginContext current login context
112      * @param httpRequest current http request
113      * @param httpResponse current http response
114      */
115     public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
116             HttpServletResponse httpResponse) {
117         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
118         httpRequest.getSession().removeAttribute(LoginContext.LOGIN_CONTEXT_KEY);
119         httpRequest.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
120         forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
121     }
122
123     /**
124      * Forwards a request to the given path.
125      * 
126      * @param forwardPath path to forward the request to
127      * @param httpRequest current HTTP request
128      * @param httpResponse current HTTP response
129      */
130     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
131             HttpServletResponse httpResponse) {
132         try {
133             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
134             dispatcher.forward(httpRequest, httpResponse);
135             return;
136         } catch (IOException e) {
137             LOG.error("Unable to return control back to authentication engine", e);
138         } catch (ServletException e) {
139             LOG.error("Unable to return control back to authentication engine", e);
140         }
141     }
142
143     /** {@inheritDoc} */
144     @SuppressWarnings("unchecked")
145     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
146             IOException {
147         LOG.debug("Processing incoming request");
148
149         if (httpResponse.isCommitted()) {
150             LOG.error("HTTP Response already committed");
151         }
152
153         LoginContext loginContext = (LoginContext) httpRequest.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
154         if (loginContext == null) {
155             // When the login context comes from the profile handlers its attached to the request
156             // The authn engine attaches it to the session to allow the handlers to do any number of
157             // request/response pairs without maintaining or losing the login context
158             loginContext = (LoginContext) httpRequest.getSession().getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
159         }
160
161         if (loginContext == null) {
162             LOG.error("Incoming request does not have attached login context");
163             throw new ServletException("Incoming request does not have attached login context");
164         }
165
166         if (!loginContext.getAuthenticationAttempted()) {
167             startUserAuthentication(loginContext, httpRequest, httpResponse);
168         } else {
169             completeAuthentication(loginContext, httpRequest, httpResponse);
170         }
171     }
172
173     /**
174      * Begins the authentication process. Determines if forced re-authentication is required or if an existing, active,
175      * authentication method is sufficient. Also determines, when authentication is required, which handler to use
176      * depending on whether passive authentication is required.
177      * 
178      * @param loginContext current login context
179      * @param httpRequest current HTTP request
180      * @param httpResponse current HTTP response
181      */
182     protected void startUserAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
183             HttpServletResponse httpResponse) {
184         LOG.debug("Beginning user authentication process");
185         try {
186             Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
187             if (idpSession != null) {
188                 LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
189             }
190
191             Map<String, LoginHandler> possibleLoginHandlers = determinePossibleLoginHandlers(loginContext);
192             LOG.debug("Possible authentication handlers for this request: {}", possibleLoginHandlers);
193
194             // Filter out possible candidate login handlers by forced and passive authentication requirements
195             if (loginContext.isForceAuthRequired()) {
196                 filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
197             }
198
199             if (loginContext.isPassiveAuthRequired()) {
200                 filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
201             }
202
203             // If the user already has a session and its usage is acceptable than use it
204             // otherwise just use the first candidate login handler
205             LOG.debug("Possible authentication handlers after filtering: {}", possibleLoginHandlers);
206             if (idpSession != null && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
207                 authenticateUserWithPreviousSession(loginContext, possibleLoginHandlers, httpRequest, httpResponse);
208             } else {
209                 possibleLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
210                 Entry<String, LoginHandler> chosenLoginHandler = possibleLoginHandlers.entrySet().iterator().next();
211                 authenticateUser(chosenLoginHandler.getKey(), chosenLoginHandler.getValue(), loginContext, httpRequest,
212                         httpResponse);
213             }
214         } catch (AuthenticationException e) {
215             loginContext.setAuthenticationFailure(e);
216             returnToProfileHandler(loginContext, httpRequest, httpResponse);
217         }
218     }
219
220     /**
221      * Determines which configured login handlers will support the requested authentication methods.
222      * 
223      * @param loginContext current login context
224      * 
225      * @return login methods that may be used to authenticate the user
226      * 
227      * @throws AuthenticationException thrown if no login handler meets the given requirements
228      */
229     protected Map<String, LoginHandler> determinePossibleLoginHandlers(LoginContext loginContext)
230             throws AuthenticationException {
231         Map<String, LoginHandler> supportedLoginHandlers = new HashMap<String, LoginHandler>(handlerManager
232                 .getLoginHandlers());
233         LOG.trace("Supported login handlers: {}", supportedLoginHandlers);
234         LOG.trace("Requested authentication methods: {}", loginContext.getRequestedAuthenticationMethods());
235
236         // If no preferences Authn method preference is given, then we're free to use any
237         if (loginContext.getRequestedAuthenticationMethods().isEmpty()) {
238             LOG.trace("No preference given for authentication methods");
239             return supportedLoginHandlers;
240         }
241
242         // Otherwise we need to filter all the mechanism supported by the IdP so that only the request types are left
243         // Previous session handler is a special case, we always to keep that around if it's configured
244         Iterator<Entry<String, LoginHandler>> supportedLoginHandlerItr = supportedLoginHandlers.entrySet().iterator();
245         Entry<String, LoginHandler> supportedLoginHandler;
246         while (supportedLoginHandlerItr.hasNext()) {
247             supportedLoginHandler = supportedLoginHandlerItr.next();
248             if (!supportedLoginHandler.getKey().equals(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)
249                     && !loginContext.getRequestedAuthenticationMethods().contains(supportedLoginHandler.getKey())) {
250                 supportedLoginHandlerItr.remove();
251                 continue;
252             }
253         }
254
255         if (supportedLoginHandlers.isEmpty()) {
256             LOG.error("No authentication method, requested by the service provider, is supported");
257             throw new AuthenticationException(
258                     "No authentication method, requested by the service provider, is supported");
259         }
260
261         return supportedLoginHandlers;
262     }
263
264     /**
265      * Filters out any login handler based on the requirement for forced authentication.
266      * 
267      * During forced authentication any handler that has not previously been used to authenticate the user or any
268      * handlers that have been and support force re-authentication may be used. Filter out any of the other ones.
269      * 
270      * @param idpSession user's current IdP session
271      * @param loginContext current login context
272      * @param loginHandlers login handlers to filter
273      * 
274      * @throws ForceAuthenticationException thrown if no handlers remain after filtering
275      */
276     protected void filterByForceAuthentication(Session idpSession, LoginContext loginContext,
277             Map<String, LoginHandler> loginHandlers) throws ForceAuthenticationException {
278         LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");
279
280         ArrayList<AuthenticationMethodInformation> activeMethods = new ArrayList<AuthenticationMethodInformation>();
281         if (idpSession != null) {
282             activeMethods.addAll(idpSession.getAuthenticationMethods().values());
283         }
284
285         loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
286
287         LoginHandler loginHandler;
288         for (AuthenticationMethodInformation activeMethod : activeMethods) {
289             loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
290             if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
291                 for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
292                     loginHandlers.remove(handlerSupportedMethods);
293                 }
294             }
295         }
296
297         LOG.debug("Authentication handlers remaining after forced authentication requirement filtering: {}",
298                 loginHandlers);
299
300         if (loginHandlers.isEmpty()) {
301             LOG.error("Force authentication required but no login handlers available to support it");
302             throw new ForceAuthenticationException();
303         }
304     }
305
306     /**
307      * Filters out any login handler that doesn't support passive authentication if the login context indicates passive
308      * authentication is required.
309      * 
310      * @param idpSession user's current IdP session
311      * @param loginContext current login context
312      * @param loginHandlers login handlers to filter
313      * 
314      * @throws PassiveAuthenticationException thrown if no handlers remain after filtering
315      */
316     protected void filterByPassiveAuthentication(Session idpSession, LoginContext loginContext,
317             Map<String, LoginHandler> loginHandlers) throws PassiveAuthenticationException {
318         LOG.debug("Passive authentication is required, filtering poassible login handlers accordingly.");
319
320         if (idpSession == null) {
321             loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
322         }
323
324         LoginHandler loginHandler;
325         Iterator<Entry<String, LoginHandler>> authnMethodItr = loginHandlers.entrySet().iterator();
326         while (authnMethodItr.hasNext()) {
327             loginHandler = authnMethodItr.next().getValue();
328             if (!loginHandler.supportsPassive()) {
329                 authnMethodItr.remove();
330             }
331         }
332
333         LOG.debug("Authentication handlers remaining after passive authentication requirement filtering: {}",
334                 loginHandlers);
335
336         if (loginHandlers.isEmpty()) {
337             LOG.error("Passive authentication required but no login handlers available to support it");
338             throw new PassiveAuthenticationException();
339         }
340     }
341
342     /**
343      * Completes the authentication request using an existing, active, authentication method for the current user.
344      * 
345      * @param loginContext current login context
346      * @param possibleLoginHandlers login handlers that meet the peers authentication requirements
347      * @param httpRequest current HTTP request
348      * @param httpResponse current HTTP response
349      */
350     protected void authenticateUserWithPreviousSession(LoginContext loginContext,
351             Map<String, LoginHandler> possibleLoginHandlers, HttpServletRequest httpRequest,
352             HttpServletResponse httpResponse) {
353         LOG.debug("Authenticating user by way of existing session.");
354
355         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
356         PreviousSessionLoginHandler loginHandler = (PreviousSessionLoginHandler) handlerManager.getLoginHandlers().get(
357                 AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
358
359         AuthenticationMethodInformation authenticationMethod = null;
360         for (String possibleAuthnMethod : idpSession.getAuthenticationMethods().keySet()) {
361             authenticationMethod = idpSession.getAuthenticationMethods().get(possibleAuthnMethod);
362             if (authenticationMethod != null) {
363                 break;
364             }
365         }
366
367         if (loginHandler.reportPreviousSessionAuthnMethod()) {
368             loginContext.setAuthenticationDuration(loginHandler.getAuthenticationDuration());
369             loginContext.setAuthenticationInstant(new DateTime());
370             loginContext.setAuthenticationMethod(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
371         } else {
372             loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
373             loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
374             loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
375         }
376         loginContext.setPrincipalName(idpSession.getPrincipalName());
377
378         loginContext.setAuthenticationAttempted();
379         httpRequest.getSession().setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
380         loginHandler.login(httpRequest, httpResponse);
381     }
382
383     /**
384      * Authenticates the user with the given authentication method provided by the given login handler.
385      * 
386      * @param authnMethod the authentication method that will be used to authenticate the user
387      * @param loginHandler login handler that will authenticate user
388      * @param loginContext current login context
389      * @param httpRequest current HTTP request
390      * @param httpResponse current HTTP response
391      */
392     protected void authenticateUser(String authnMethod, LoginHandler loginHandler, LoginContext loginContext,
393             HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
394         LOG.debug("Authenticating user with login handler of type {}", loginHandler.getClass().getName());
395
396         loginContext.setAuthenticationAttempted();
397         loginContext.setAuthenticationInstant(new DateTime());
398         loginContext.setAuthenticationDuration(loginHandler.getAuthenticationDuration());
399         loginContext.setAuthenticationMethod(authnMethod);
400         loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
401         httpRequest.getSession().setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginContext);
402         loginHandler.login(httpRequest, httpResponse);
403     }
404
405     /**
406      * Completes the authentication process.
407      * 
408      * The principal name set by the authentication handler is retrieved and pushed in to the login context, a
409      * Shibboleth session is created if needed, information indicating that the user has logged into the service is
410      * recorded and finally control is returned back to the profile handler.
411      * 
412      * @param loginContext current login context
413      * @param httpRequest current HTTP request
414      * @param httpResponse current HTTP response
415      */
416     protected void completeAuthentication(LoginContext loginContext, HttpServletRequest httpRequest,
417             HttpServletResponse httpResponse) {
418         LOG.debug("Completing user authentication process");
419
420         // We check if the principal name was already set in the login context
421         // if not attempt to pull it from where login handlers are supposed to provide it
422         String principalName = DatatypeHelper.safeTrimOrNullString(loginContext.getPrincipalName());
423         if (principalName == null) {
424             principalName = DatatypeHelper.safeTrimOrNullString((String) httpRequest
425                     .getAttribute(LoginHandler.PRINCIPAL_NAME_KEY));
426             if (principalName != null) {
427                 loginContext.setPrincipalName(principalName);
428             } else {
429                 loginContext.setPrincipalAuthenticated(false);
430                 loginContext.setAuthenticationFailure(new AuthenticationException(
431                         "No principal name returned from authentication handler."));
432                 LOG.error("No principal name returned from authentication method: "
433                         + loginContext.getAuthenticationMethod());
434                 returnToProfileHandler(loginContext, httpRequest, httpResponse);
435                 return;
436             }
437         }
438         loginContext.setPrincipalAuthenticated(true);
439
440         // We allow a login handler to override the authentication method in the event that it supports multiple methods
441         String actualAuthnMethod = DatatypeHelper.safeTrimOrNullString((String) httpRequest
442                 .getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY));
443         if (actualAuthnMethod != null) {
444             loginContext.setAuthenticationMethod(actualAuthnMethod);
445         }
446
447         LOG.debug("User {} authenticated with method {}", loginContext.getPrincipalName(), loginContext
448                 .getAuthenticationMethod());
449         updateUserSession(loginContext, httpRequest, httpResponse);
450         returnToProfileHandler(loginContext, httpRequest, httpResponse);
451     }
452
453     /**
454      * Updates the user's Shibboleth session with authentication information. If no session exists a new one will be
455      * created.
456      * 
457      * @param loginContext current login context
458      * @param httpRequest current HTTP request
459      * @param httpResponse current HTTP response
460      */
461     protected void updateUserSession(LoginContext loginContext, HttpServletRequest httpRequest,
462             HttpServletResponse httpResponse) {
463         Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
464         if (idpSession == null) {
465             LOG.debug("Creating shibboleth session for principal {}", loginContext.getPrincipalName());
466             idpSession = (Session) sessionManager.createSession(loginContext.getPrincipalName());
467             loginContext.setSessionID(idpSession.getSessionID());
468             addSessionCookie(httpRequest, httpResponse, idpSession);
469         }
470
471         LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
472                 loginContext.getPrincipalName());
473         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
474         String authnMethod = (String) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY);
475         if (DatatypeHelper.isEmpty(authnMethod)) {
476             authnMethod = loginContext.getAuthenticationMethod();
477         }
478
479         AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
480                 loginContext.getAuthenticationInstant(), loginContext.getAuthenticationDuration());
481
482         idpSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
483
484         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
485                 authnMethodInfo);
486         idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
487     }
488
489     /**
490      * Adds an IdP session cookie to the outbound response.
491      * 
492      * @param httpRequest current request
493      * @param httpResponse current response
494      * @param userSession user's session
495      */
496     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
497             Session userSession) {
498         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
499
500         LOG.debug("Adding IdP session cookie to HTTP response");
501         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, userSession.getSessionID());
502         
503         String contextPath = httpRequest.getContextPath();
504         if(DatatypeHelper.isEmpty(contextPath)){
505             sessionCookie.setPath("/");
506         }else{
507             sessionCookie.setPath(contextPath);
508         }
509         
510         sessionCookie.setSecure(httpRequest.isSecure());
511         sessionCookie.setMaxAge(-1);
512
513         httpResponse.addCookie(sessionCookie);
514     }
515 }