Down level filter logging messages
[java-idp.git] / src / 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.List;
21
22 import javax.security.auth.Subject;
23 import javax.servlet.RequestDispatcher;
24 import javax.servlet.ServletException;
25 import javax.servlet.http.Cookie;
26 import javax.servlet.http.HttpServlet;
27 import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse;
29 import javax.servlet.http.HttpSession;
30
31 import org.joda.time.DateTime;
32 import org.opensaml.xml.util.DatatypeHelper;
33 import org.opensaml.xml.util.Pair;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
38 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
39 import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
40 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
41 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
42 import edu.internet2.middleware.shibboleth.idp.session.Session;
43 import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
44 import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
45
46 /**
47  * Manager responsible for handling authentication requests.
48  */
49 public class AuthenticationEngine extends HttpServlet {
50
51     /** Name of the IdP Cookie containing the IdP session ID. */
52     public static final String IDP_SESSION_COOKIE_NAME = "_idp_session";
53
54     /** Serial version UID. */
55     private static final long serialVersionUID = 8494202791991613148L;
56
57     /** Class logger. */
58     private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
59
60     /**
61      * Gets the manager used to retrieve handlers for requests.
62      * 
63      * @return manager used to retrieve handlers for requests
64      */
65     public IdPProfileHandlerManager getProfileHandlerManager() {
66         return (IdPProfileHandlerManager) getServletContext().getAttribute("handlerManager");
67     }
68
69     /**
70      * Gets the session manager to be used.
71      * 
72      * @return session manager to be used
73      */
74     @SuppressWarnings("unchecked")
75     public SessionManager<Session> getSessionManager() {
76         return (SessionManager<Session>) getServletContext().getAttribute("sessionManager");
77     }
78
79     /**
80      * Returns control back to the authentication engine.
81      * 
82      * @param httpRequest current http request
83      * @param httpResponse current http response
84      */
85     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
86         LOG.debug("Returning control to authentication engine");
87         HttpSession httpSession = httpRequest.getSession();
88         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
89         if (loginContext == null) {
90             LOG.error("User HttpSession did not contain a login context.  Unable to return to authentication engine");
91         }
92         forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
93     }
94
95     /**
96      * Returns control back to the profile handler that invoked the authentication engine.
97      * 
98      * @param loginContext current login context
99      * @param httpRequest current http request
100      * @param httpResponse current http response
101      */
102     public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
103             HttpServletResponse httpResponse) {
104         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
105         forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
106     }
107
108     /**
109      * Forwards a request to the given path.
110      * 
111      * @param forwardPath path to forward the request to
112      * @param httpRequest current HTTP request
113      * @param httpResponse current HTTP response
114      */
115     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
116             HttpServletResponse httpResponse) {
117         try {
118             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
119             dispatcher.forward(httpRequest, httpResponse);
120             return;
121         } catch (IOException e) {
122             LOG.error("Unable to return control back to authentication engine", e);
123         } catch (ServletException e) {
124             LOG.error("Unable to return control back to authentication engine", e);
125         }
126     }
127
128     /** {@inheritDoc} */
129     @SuppressWarnings("unchecked")
130     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
131             IOException {
132         LOG.debug("Processing incoming request");
133
134         if (httpResponse.isCommitted()) {
135             LOG.error("HTTP Response already committed");
136         }
137
138         HttpSession httpSession = httpRequest.getSession();
139         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
140         if (loginContext == null) {
141             LOG.error("Incoming request does not have attached login context");
142             throw new ServletException("Incoming request does not have attached login context");
143         }
144
145         if (!loginContext.getAuthenticationAttempted()) {
146             Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
147
148             AuthenticationMethodInformation authenticationMethod = getUsableExistingAuthenticationMethod(loginContext,
149                     shibSession);
150             if (authenticationMethod != null) {
151                 LOG.debug("An active authentication method is applicable for relying party.  Using authentication "
152                         + "method {} as authentication method to relying party without re-authenticating user.",
153                         authenticationMethod.getAuthenticationMethod());
154                 authenticateUserWithActiveMethod(httpRequest, httpResponse, authenticationMethod);
155                 return;
156             }
157
158             LOG.debug("No active authentication method is applicable for relying party.  "
159                     + "Authenticating user with to be determined method.");
160             authenticateUserWithoutActiveMethod1(httpRequest, httpResponse);
161         } else {
162             LOG.debug("Request returned from authentication handler, completing authentication process.");
163             authenticateUserWithoutActiveMethod2(httpRequest, httpResponse);
164         }
165
166         return;
167     }
168
169     /**
170      * Completes the authentication request using an existing, active, authentication method for the current user.
171      * 
172      * @param httpRequest current HTTP request
173      * @param httpResponse current HTTP response
174      * @param authenticationMethod authentication method to use to complete the request
175      */
176     protected void authenticateUserWithActiveMethod(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
177             AuthenticationMethodInformation authenticationMethod) {
178         HttpSession httpSession = httpRequest.getSession();
179
180         Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
181
182         LOG.debug("Populating login context with existing session and authentication method information.");
183         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
184         loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
185         loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
186         loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
187         loginContext.setPrincipalAuthenticated(true);
188         loginContext.setPrincipalName(shibSession.getPrincipalName());
189
190         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
191                 authenticationMethod);
192         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
193
194         returnToProfileHandler(loginContext, httpRequest, httpResponse);
195     }
196
197     /**
198      * Performs the first part of user authentication. An authentication handler is determined, the login context is
199      * populated with some initial information, and control is forward to the selected handler so that it may
200      * authenticate the user.
201      * 
202      * @param httpRequest current HTTP request
203      * @param httpResponse current HTTP response
204      */
205     protected void authenticateUserWithoutActiveMethod1(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
206         HttpSession httpSession = httpRequest.getSession();
207         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
208         LOG.debug("Selecting appropriate authentication method for request.");
209         Pair<String, LoginHandler> handler = getProfileHandlerManager().getAuthenticationHandler(loginContext);
210
211         if (handler == null) {
212             loginContext.setPrincipalAuthenticated(false);
213             loginContext.setAuthenticationAttempted();
214             loginContext.setAuthenticationFailureMessage("No AuthenticationHandler satisfies the request from: "
215                     + loginContext.getRelyingPartyId());
216             LOG.error("No AuthenticationHandler satisfies the request from relying party: "
217                     + loginContext.getRelyingPartyId());
218             returnToProfileHandler(loginContext, httpRequest, httpResponse);
219             return;
220         }
221
222         LOG.debug("Authentication method {} will be used to authenticate user.", handler.getFirst());
223         loginContext.setAuthenticationAttempted();
224         loginContext.setAuthenticationDuration(handler.getSecond().getAuthenticationDuration());
225         loginContext.setAuthenticationMethod(handler.getFirst());
226         loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
227
228         LOG.debug("Transferring control to authentication handler of type: {}", handler.getSecond().getClass()
229                 .getName());
230         handler.getSecond().login(httpRequest, httpResponse);
231     }
232
233     /**
234      * Performs the second part of user authentication. The principal name set by the authentication handler is
235      * retrieved and pushed in to the login context, a Shibboleth session is created if needed, information indicating
236      * that the user has logged into the service is recorded and finally control is returned back to the profile
237      * handler.
238      * 
239      * @param httpRequest current HTTP request
240      * @param httpResponse current HTTP response
241      */
242     protected void authenticateUserWithoutActiveMethod2(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
243         HttpSession httpSession = httpRequest.getSession();
244
245         String principalName = (String) httpRequest.getAttribute(LoginHandler.PRINCIPAL_NAME_KEY);
246         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
247         if (DatatypeHelper.isEmpty(principalName)) {
248             loginContext.setPrincipalAuthenticated(false);
249             loginContext.setAuthenticationFailureMessage("No principal name returned from authentication handler.");
250             LOG.error("No principal name returned from authentication method: "
251                     + loginContext.getAuthenticationMethod());
252             returnToProfileHandler(loginContext, httpRequest, httpResponse);
253             return;
254         }
255         loginContext.setPrincipalAuthenticated(true);
256         loginContext.setPrincipalName(principalName);
257         loginContext.setAuthenticationInstant(new DateTime());
258
259         Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
260         if (shibSession == null) {
261             LOG.debug("Creating shibboleth session for principal {}", principalName);
262             shibSession = (Session) getSessionManager().createSession(loginContext.getPrincipalName());
263             loginContext.setSessionID(shibSession.getSessionID());
264             addSessionCookie(httpRequest, httpResponse, shibSession);
265         }
266
267         LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
268                 principalName);
269         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
270         String authnMethod = (String) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY);
271         if (DatatypeHelper.isEmpty(authnMethod)) {
272             authnMethod = loginContext.getAuthenticationMethod();
273         }
274
275         AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
276                 new DateTime(), loginContext.getAuthenticationDuration());
277
278         shibSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
279
280         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
281                 authnMethodInfo);
282         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
283
284         returnToProfileHandler(loginContext, httpRequest, httpResponse);
285     }
286
287     /**
288      * Gets the authentication method, currently active for the user, that also meets the requirements expressed by the
289      * login context. If a method is returned the user does not need to authenticate again, if null is returned then the
290      * user must be authenticated.
291      * 
292      * @param loginContext user login context
293      * @param shibSession user's shibboleth session
294      * 
295      * @return active authentication method that meets authentication requirements or null
296      */
297     protected AuthenticationMethodInformation getUsableExistingAuthenticationMethod(LoginContext loginContext,
298             Session shibSession) {
299
300         if (shibSession == null) {
301             LOG.debug("No existing authentication methods due to the lack of existing IdP sessions");
302             return null;
303         }
304
305         if (loginContext.getForceAuth()) {
306             LOG.debug("Request for forced re-authentication, no existing authentication method considered usable");
307             return null;
308         }
309
310         List<String> preferredAuthnMethods = loginContext.getRequestedAuthenticationMethods();
311         AuthenticationMethodInformation authnMethodInformation = null;
312         if (preferredAuthnMethods == null || preferredAuthnMethods.size() == 0) {
313             for (AuthenticationMethodInformation info : shibSession.getAuthenticationMethods().values()) {
314                 if (!info.isExpired()) {
315                     authnMethodInformation = info;
316                     break;
317                 }
318             }
319         } else {
320             for (String preferredAuthnMethod : preferredAuthnMethods) {
321                 if (shibSession.getAuthenticationMethods().containsKey(preferredAuthnMethod)) {
322                     AuthenticationMethodInformation info = shibSession.getAuthenticationMethods().get(
323                             preferredAuthnMethod);
324                     if (!info.isExpired()) {
325                         authnMethodInformation = info;
326                         break;
327                     }
328                 }
329             }
330         }
331
332         return authnMethodInformation;
333     }
334
335     /**
336      * Adds an IdP session cookie to the outbound response.
337      * 
338      * @param httpRequest current request
339      * @param httpResponse current response
340      * @param userSession user's session
341      */
342     protected void addSessionCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
343             Session userSession) {
344         httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);
345
346         LOG.debug("Adding IdP session cookie to HTTP response");
347         Cookie sessionCookie = new Cookie(IDP_SESSION_COOKIE_NAME, userSession.getSessionID());
348         sessionCookie.setPath(httpRequest.getContextPath());
349         sessionCookie.setSecure(false);
350
351         int maxAge = (int) (userSession.getInactivityTimeout() / 1000);
352         sessionCookie.setMaxAge(maxAge);
353
354         httpResponse.addCookie(sessionCookie);
355     }
356 }