fix redirect loop within authentication engine
[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.joda.time.DateTime;
33 import org.opensaml.xml.util.DatatypeHelper;
34 import org.opensaml.xml.util.Pair;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import edu.internet2.middleware.shibboleth.common.session.SessionManager;
39 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
40 import edu.internet2.middleware.shibboleth.idp.profile.IdPProfileHandlerManager;
41 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
42 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
43 import edu.internet2.middleware.shibboleth.idp.session.Session;
44 import edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl;
45 import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
46
47 /**
48  * Manager responsible for handling authentication requests.
49  */
50 public class AuthenticationEngine extends HttpServlet {
51
52     /** Serial version UID. */
53     private static final long serialVersionUID = 8494202791991613148L;
54
55     /** Class logger. */
56     private static final Logger LOG = LoggerFactory.getLogger(AuthenticationEngine.class);
57
58     /**
59      * Gets the manager used to retrieve handlers for requests.
60      * 
61      * @return manager used to retrieve handlers for requests
62      */
63     public IdPProfileHandlerManager getProfileHandlerManager() {
64         return (IdPProfileHandlerManager) getServletContext().getAttribute("handlerManager");
65     }
66
67     /**
68      * Gets the session manager to be used.
69      * 
70      * @return session manager to be used
71      */
72     @SuppressWarnings("unchecked")
73     public SessionManager<Session> getSessionManager() {
74         return (SessionManager<Session>) getServletContext().getAttribute("sessionManager");
75     }
76
77     /**
78      * Returns control back to the authentication engine.
79      * 
80      * @param httpRequest current http request
81      * @param httpResponse current http response
82      */
83     public static void returnToAuthenticationEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
84         LOG.debug("Returning control to authentication engine");
85         HttpSession httpSession = httpRequest.getSession();
86         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
87         if (loginContext == null) {
88             LOG.error("User HttpSession did not contain a login context.  Unable to return to authentication engine");
89         }
90         forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
91     }
92
93     /**
94      * Returns control back to the profile handler that invoked the authentication engine.
95      * 
96      * @param loginContext current login context
97      * @param httpRequest current http request
98      * @param httpResponse current http response
99      */
100     public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
101             HttpServletResponse httpResponse) {
102         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
103         forwardRequest(loginContext.getProfileHandlerURL(), httpRequest, httpResponse);
104     }
105
106     /**
107      * Forwards a request to the given path.
108      * 
109      * @param forwardPath path to forward the request to
110      * @param httpRequest current HTTP request
111      * @param httpResponse current HTTP response
112      */
113     protected static void forwardRequest(String forwardPath, HttpServletRequest httpRequest,
114             HttpServletResponse httpResponse) {
115         try {
116             RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(forwardPath);
117             dispatcher.forward(httpRequest, httpResponse);
118             return;
119         } catch (IOException e) {
120             LOG.error("Unable to return control back to authentication engine", e);
121         } catch (ServletException e) {
122             LOG.error("Unable to return control back to authentication engine", e);
123         }
124     }
125
126     /** {@inheritDoc} */
127     @SuppressWarnings("unchecked")
128     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
129             IOException {
130         LOG.debug("Processing incoming request");
131
132         if (httpResponse.isCommitted()) {
133             LOG.error("HTTP Response already committed");
134         }
135
136         HttpSession httpSession = httpRequest.getSession();
137         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
138         if (loginContext == null) {
139             LOG.error("Incoming request does not have attached login context");
140             throw new ServletException("Incoming request does not have attached login context");
141         }
142
143         if (!loginContext.getAuthenticationAttempted()) {
144             String shibSessionId = (String) httpSession.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
145             Session shibSession = getSessionManager().getSession(shibSessionId);
146
147             AuthenticationMethodInformation authenticationMethod = getUsableExistingAuthenticationMethod(loginContext,
148                     shibSession);
149             if (authenticationMethod != null) {
150                 LOG.debug("An active authentication method is applicable for relying party.  Using authentication "
151                         + "method {} as authentication method to relying party without re-authenticating user.",
152                         authenticationMethod.getAuthenticationMethod());
153                 authenticateUserWithActiveMethod(httpRequest, httpResponse, authenticationMethod);
154                 return;
155             }
156
157             LOG.debug("No active authentication method is applicable for relying party.  "
158                     + "Authenticating user with to be determined method.");
159             authenticateUserWithoutActiveMethod1(httpRequest, httpResponse);
160         } else {
161             LOG.debug("Request returned from authentication handler, completing authentication process.");
162             authenticateUserWithoutActiveMethod2(httpRequest, httpResponse);
163         }
164
165         return;
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         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         String shibSessionId = (String) httpSession.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
260         Session shibSession = getSessionManager().getSession(shibSessionId);
261
262         if (shibSession == null) {
263             LOG.debug("Creating shibboleth session for principal {}", principalName);
264
265             InetAddress addr;
266             try {
267                 addr = InetAddress.getByName(httpRequest.getRemoteAddr());
268             } catch (UnknownHostException ex) {
269                 addr = null;
270             }
271
272             shibSession = (Session) getSessionManager().createSession(addr, loginContext.getPrincipalName());
273             loginContext.setSessionID(shibSession.getSessionID());
274             httpSession.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, shibSession.getSessionID());
275         }
276
277         LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
278                 principalName);
279         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
280         String authnMethod = (String) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY);
281         if (DatatypeHelper.isEmpty(authnMethod)) {
282             authnMethod = loginContext.getAuthenticationMethod();
283         }
284
285         AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
286                 new DateTime(), loginContext.getAuthenticationDuration());
287
288         shibSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
289
290         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
291                 authnMethodInfo);
292         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
293
294         shibSession.setLastActivityInstant(new DateTime());
295
296         returnToProfileHandler(loginContext, httpRequest, httpResponse);
297     }
298
299     /**
300      * Gets the authentication method, currently active for the user, that also meets the requirements expressed by the
301      * login context. If a method is returned the user does not need to authenticate again, if null is returned then the
302      * user must be authenticated.
303      * 
304      * @param loginContext user login context
305      * @param shibSession user's shibboleth session
306      * 
307      * @return active authentication method that meets authentication requirements or null
308      */
309     protected AuthenticationMethodInformation getUsableExistingAuthenticationMethod(LoginContext loginContext,
310             Session shibSession) {
311
312         if (shibSession == null) {
313             return null;
314         }
315
316         if (loginContext.getForceAuth()) {
317             LOG.debug("Request for forced re-authentication, no existing authentication method considered usable");
318             return null;
319         }
320
321         List<String> preferredAuthnMethods = loginContext.getRequestedAuthenticationMethods();
322         AuthenticationMethodInformation authnMethodInformation = null;
323         if (preferredAuthnMethods == null || preferredAuthnMethods.size() == 0) {
324             for (AuthenticationMethodInformation info : shibSession.getAuthenticationMethods().values()) {
325                 if (!info.isExpired()) {
326                     authnMethodInformation = info;
327                     break;
328                 }
329             }
330         } else {
331             for (String preferredAuthnMethod : preferredAuthnMethods) {
332                 if (shibSession.getAuthenticationMethods().containsKey(preferredAuthnMethod)) {
333                     AuthenticationMethodInformation info = shibSession.getAuthenticationMethods().get(
334                             preferredAuthnMethod);
335                     if (!info.isExpired()) {
336                         authnMethodInformation = info;
337                         break;
338                     }
339                 }
340             }
341         }
342
343         return authnMethodInformation;
344     }
345 }