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