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