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