Bind session to request once authentication is complete
[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.HttpServlet;
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28 import javax.servlet.http.HttpSession;
29
30 import org.joda.time.DateTime;
31 import org.opensaml.xml.util.DatatypeHelper;
32 import org.opensaml.xml.util.Pair;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
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 = LoggerFactory.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         LOG.debug("Returning control to authentication engine");
83         HttpSession httpSession = httpRequest.getSession();
84         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
85         if (loginContext == null) {
86             LOG.error("User HttpSession did not contain a login context.  Unable to return to authentication engine");
87         }
88         forwardRequest(loginContext.getAuthenticationEngineURL(), httpRequest, httpResponse);
89     }
90
91     /**
92      * Returns control back to the profile handler that invoked the authentication engine.
93      * 
94      * @param loginContext current login context
95      * @param httpRequest current http request
96      * @param httpResponse current http response
97      */
98     public static void returnToProfileHandler(LoginContext loginContext, HttpServletRequest httpRequest,
99             HttpServletResponse httpResponse) {
100         LOG.debug("Returning control to profile handler at: {}", loginContext.getProfileHandlerURL());
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             return;
117         } catch (IOException e) {
118             LOG.error("Unable to return control back to authentication engine", e);
119         } catch (ServletException e) {
120             LOG.error("Unable to return control back to authentication engine", e);
121         }
122     }
123
124     /** {@inheritDoc} */
125     @SuppressWarnings("unchecked")
126     protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException,
127             IOException {
128         LOG.debug("Processing incoming request");
129
130         if (httpResponse.isCommitted()) {
131             LOG.error("HTTP Response already committed");
132         }
133
134         HttpSession httpSession = httpRequest.getSession();
135         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
136         if (loginContext == null) {
137             LOG.error("Incoming request does not have attached login context");
138             throw new ServletException("Incoming request does not have attached login context");
139         }
140
141         if (!loginContext.getAuthenticationAttempted()) {
142             Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
143
144             AuthenticationMethodInformation authenticationMethod = getUsableExistingAuthenticationMethod(loginContext,
145                     shibSession);
146             if (authenticationMethod != null) {
147                 LOG.debug("An active authentication method is applicable for relying party.  Using authentication "
148                         + "method {} as authentication method to relying party without re-authenticating user.",
149                         authenticationMethod.getAuthenticationMethod());
150                 authenticateUserWithActiveMethod(httpRequest, httpResponse, authenticationMethod);
151                 return;
152             }
153
154             LOG.debug("No active authentication method is applicable for relying party.  "
155                     + "Authenticating user with to be determined method.");
156             authenticateUserWithoutActiveMethod1(httpRequest, httpResponse);
157         } else {
158             LOG.debug("Request returned from authentication handler, completing authentication process.");
159             authenticateUserWithoutActiveMethod2(httpRequest, httpResponse);
160         }
161
162         return;
163     }
164
165     /**
166      * Completes the authentication request using an existing, active, authentication method for the current user.
167      * 
168      * @param httpRequest current HTTP request
169      * @param httpResponse current HTTP response
170      * @param authenticationMethod authentication method to use to complete the request
171      */
172     protected void authenticateUserWithActiveMethod(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
173             AuthenticationMethodInformation authenticationMethod) {
174         HttpSession httpSession = httpRequest.getSession();
175
176         Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
177
178         LOG.debug("Populating login context with existing session and authentication method information.");
179         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
180         loginContext.setAuthenticationDuration(authenticationMethod.getAuthenticationDuration());
181         loginContext.setAuthenticationInstant(authenticationMethod.getAuthenticationInstant());
182         loginContext.setAuthenticationMethod(authenticationMethod.getAuthenticationMethod());
183         loginContext.setPrincipalAuthenticated(true);
184         loginContext.setPrincipalName(shibSession.getPrincipalName());
185
186         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
187                 authenticationMethod);
188         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
189
190         returnToProfileHandler(loginContext, httpRequest, httpResponse);
191     }
192
193     /**
194      * Performs the first part of user authentication. An authentication handler is determined, the login context is
195      * populated with some initial information, and control is forward to the selected handler so that it may
196      * authenticate the user.
197      * 
198      * @param httpRequest current HTTP request
199      * @param httpResponse current HTTP response
200      */
201     protected void authenticateUserWithoutActiveMethod1(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
202         HttpSession httpSession = httpRequest.getSession();
203         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
204         LOG.debug("Selecting appropriate authentication method for request.");
205         Pair<String, LoginHandler> handler = getProfileHandlerManager().getAuthenticationHandler(loginContext);
206
207         if (handler == null) {
208             loginContext.setPrincipalAuthenticated(false);
209             loginContext.setAuthenticationAttempted();
210             loginContext.setAuthenticationFailureMessage("No AuthenticationHandler satisfies the request from: "
211                     + loginContext.getRelyingPartyId());
212             LOG.error("No AuthenticationHandler satisfies the request from relying party: "
213                     + loginContext.getRelyingPartyId());
214             returnToProfileHandler(loginContext, httpRequest, httpResponse);
215             return;
216         }
217
218         LOG.debug("Authentication method {} will be used to authenticate user.", handler.getFirst());
219         loginContext.setAuthenticationAttempted();
220         loginContext.setAuthenticationDuration(handler.getSecond().getAuthenticationDuration());
221         loginContext.setAuthenticationMethod(handler.getFirst());
222         loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
223
224         LOG.debug("Transferring control to authentication handler of type: {}", handler.getSecond().getClass()
225                 .getName());
226         handler.getSecond().login(httpRequest, httpResponse);
227     }
228
229     /**
230      * Performs the second part of user authentication. The principal name set by the authentication handler is
231      * retrieved and pushed in to the login context, a Shibboleth session is created if needed, information indicating
232      * that the user has logged into the service is recorded and finally control is returned back to the profile
233      * handler.
234      * 
235      * @param httpRequest current HTTP request
236      * @param httpResponse current HTTP response
237      */
238     protected void authenticateUserWithoutActiveMethod2(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
239         HttpSession httpSession = httpRequest.getSession();
240
241         String principalName = (String) httpRequest.getAttribute(LoginHandler.PRINCIPAL_NAME_KEY);
242         LoginContext loginContext = (LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
243         if (DatatypeHelper.isEmpty(principalName)) {
244             loginContext.setPrincipalAuthenticated(false);
245             loginContext.setAuthenticationFailureMessage("No principal name returned from authentication handler.");
246             LOG.error("No principal name returned from authentication method: "
247                     + loginContext.getAuthenticationMethod());
248             returnToProfileHandler(loginContext, httpRequest, httpResponse);
249             return;
250         }
251         loginContext.setPrincipalAuthenticated(true);
252         loginContext.setPrincipalName(principalName);
253         loginContext.setAuthenticationInstant(new DateTime());
254
255         Session shibSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
256         if (shibSession == null) {
257             LOG.debug("Creating shibboleth session for principal {}", principalName);
258             shibSession = (Session) getSessionManager().createSession(loginContext.getPrincipalName());
259             loginContext.setSessionID(shibSession.getSessionID());
260             httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, shibSession);
261         }
262
263         LOG.debug("Recording authentication and service information in Shibboleth session for principal: {}",
264                 principalName);
265         Subject subject = (Subject) httpRequest.getAttribute(LoginHandler.SUBJECT_KEY);
266         String authnMethod = (String) httpRequest.getAttribute(LoginHandler.AUTHENTICATION_METHOD_KEY);
267         if (DatatypeHelper.isEmpty(authnMethod)) {
268             authnMethod = loginContext.getAuthenticationMethod();
269         }
270
271         AuthenticationMethodInformation authnMethodInfo = new AuthenticationMethodInformationImpl(subject, authnMethod,
272                 new DateTime(), loginContext.getAuthenticationDuration());
273
274         shibSession.getAuthenticationMethods().put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
275
276         ServiceInformation serviceInfo = new ServiceInformationImpl(loginContext.getRelyingPartyId(), new DateTime(),
277                 authnMethodInfo);
278         shibSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
279
280         shibSession.setLastActivityInstant(new DateTime());
281
282         returnToProfileHandler(loginContext, httpRequest, httpResponse);
283     }
284
285     /**
286      * Gets the authentication method, currently active for the user, that also meets the requirements expressed by the
287      * login context. If a method is returned the user does not need to authenticate again, if null is returned then the
288      * user must be authenticated.
289      * 
290      * @param loginContext user login context
291      * @param shibSession user's shibboleth session
292      * 
293      * @return active authentication method that meets authentication requirements or null
294      */
295     protected AuthenticationMethodInformation getUsableExistingAuthenticationMethod(LoginContext loginContext,
296             Session shibSession) {
297
298         if (shibSession == null) {
299             return null;
300         }
301
302         if (loginContext.getForceAuth()) {
303             LOG.debug("Request for forced re-authentication, no existing authentication method considered usable");
304             return null;
305         }
306
307         List<String> preferredAuthnMethods = loginContext.getRequestedAuthenticationMethods();
308         AuthenticationMethodInformation authnMethodInformation = null;
309         if (preferredAuthnMethods == null || preferredAuthnMethods.size() == 0) {
310             for (AuthenticationMethodInformation info : shibSession.getAuthenticationMethods().values()) {
311                 if (!info.isExpired()) {
312                     authnMethodInformation = info;
313                     break;
314                 }
315             }
316         } else {
317             for (String preferredAuthnMethod : preferredAuthnMethods) {
318                 if (shibSession.getAuthenticationMethods().containsKey(preferredAuthnMethod)) {
319                     AuthenticationMethodInformation info = shibSession.getAuthenticationMethods().get(
320                             preferredAuthnMethod);
321                     if (!info.isExpired()) {
322                         authnMethodInformation = info;
323                         break;
324                     }
325                 }
326             }
327         }
328
329         return authnMethodInformation;
330     }
331 }