View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    * 
4    * This program is free software; you can
5    * redistribute it and/or modify it under the terms of the GNU General Public
6    * License as published by the Free Software Foundation; either version 2 of the
7    * License, or (at your option) any later version. This program is distributed
8    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
9    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10   * See the GNU General Public License for more details. You should have received
11   * a copy of the GNU General Public License along with this program; if not,
12   * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
13   * Boston, MA 02111-1307, USA.
14   * 
15   * $Id: LoginServlet.java,v 1.51 2006/01/16 16:06:43 indal Exp $
16   */
17  
18  package no.feide.moria.servlet;
19  
20  import java.io.IOException;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.Map;
24  import java.util.Properties;
25  import java.util.ResourceBundle;
26  import java.util.TreeMap;
27  
28  import javax.servlet.RequestDispatcher;
29  import javax.servlet.ServletException;
30  import javax.servlet.http.Cookie;
31  import javax.servlet.http.HttpServlet;
32  import javax.servlet.http.HttpServletRequest;
33  import javax.servlet.http.HttpServletResponse;
34  
35  import no.feide.moria.authorization.UnknownServicePrincipalException;
36  import no.feide.moria.controller.AuthenticationException;
37  import no.feide.moria.controller.AuthorizationException;
38  import no.feide.moria.controller.DirectoryUnavailableException;
39  import no.feide.moria.controller.IllegalInputException;
40  import no.feide.moria.controller.InoperableStateException;
41  import no.feide.moria.controller.MoriaController;
42  import no.feide.moria.controller.MoriaControllerException;
43  import no.feide.moria.controller.UnknownTicketException;
44  import no.feide.moria.log.MessageLogger;
45  
46  /***
47   * Use this servlet to bootstrap the system. Set
48   * <load-on-startup>1</load-on-startup> in web.xml.
49   * @author Lars Preben S. Arnesen <lars.preben.arnesen@conduct.no>
50   * @version $Revision: 1.51 $
51   */
52  public final class LoginServlet
53  extends MoriaServlet {
54  
55      /*** Used for logging. */
56      private final MessageLogger log = new MessageLogger(LoginServlet.class);
57  
58      /***
59       * List of parameters required by <code>LoginServlet</code>. <br>
60       * <br>
61       * Current required parameters are:
62       * <ul>
63       * <li><code>RequestUtil.PROP_COOKIE_SSO</code>
64       * <li><code>RequestUtil.PROP_COOKIE_SSO_TTL</code>
65       * <li><code>RequestUtil.PROP_COOKIE_DENYSSO</code>
66       * <li><code>RequestUtil.PROP_COOKIE_DENYSSO_TTL</code>
67       * <li><code>RequestUtil.PROP_COOKIE_LANG</code>
68       * <li><code>RequestUtil.PROP_COOKIE_LANG_TTL</code>
69       * <li><code>RequestUtil.PROP_COOKIE_ORG</code>
70       * <li><code>RequestUtil.PROP_COOKIE_ORG_TTL</code>
71       * <li><code>RequestUtil.PROP_LOGIN_TICKET_PARAM</code>
72       * <li><code>RequestUtil.PROP_LOGIN_DEFAULT_LANGUAGE</code>
73       * <li><code>RequestUtil.PROP_LOGIN_URL_PREFIX</code>
74       * <li><code>RequestUtil.FAQ_LINK</code>
75       * <li><code>RequestUtil.PROP_FAQ_STATUS</code>
76       * <li><code>RequestUtil.PROP_FAQ_OWNER</code>
77       * </ul>
78       * @see RequestUtil#PROP_COOKIE_SSO
79       * @see RequestUtil#PROP_COOKIE_SSO_TTL
80       * @see RequestUtil#PROP_COOKIE_DENYSSO
81       * @see RequestUtil#PROP_COOKIE_DENYSSO_TTL
82       * @see RequestUtil#PROP_COOKIE_LANG
83       * @see RequestUtil#PROP_COOKIE_LANG_TTL
84       * @see RequestUtil#PROP_COOKIE_ORG
85       * @see RequestUtil#PROP_COOKIE_ORG_TTL
86       * @see RequestUtil#PROP_LOGIN_TICKET_PARAM
87       * @see RequestUtil#PROP_LOGIN_DEFAULT_LANGUAGE
88       * @see RequestUtil#PROP_LOGIN_URL_PREFIX
89       * @see RequestUtil#PROP_FAQ_STATUS
90       */
91      private static final String[] REQUIRED_PARAMETERS = {
92                                                           RequestUtil.PROP_COOKIE_DENYSSO,
93                                                           RequestUtil.PROP_LOGIN_TICKET_PARAM,
94                                                           RequestUtil.PROP_COOKIE_SSO,
95                                                           RequestUtil.PROP_COOKIE_LANG,
96                                                           RequestUtil.PROP_LOGIN_DEFAULT_LANGUAGE,
97                                                           RequestUtil.PROP_COOKIE_DENYSSO_TTL,
98                                                           RequestUtil.PROP_COOKIE_ORG,
99                                                           RequestUtil.PROP_COOKIE_ORG_TTL,
100                                                          RequestUtil.PROP_COOKIE_SSO_TTL,
101                                                          RequestUtil.PROP_COOKIE_LANG_TTL,
102                                                          RequestUtil.PROP_LOGIN_URL_PREFIX,
103                                                          RequestUtil.FAQ_LINK,
104                                                          RequestUtil.PROP_FAQ_STATUS};
105 
106     /***
107      * 
108      * @return The required parameters for this servlet.
109      */
110     public static String[] getRequiredParameters() {
111         return REQUIRED_PARAMETERS;
112     }
113 
114     /***
115      * Handles the GET request. The GET request should contain a login ticket as
116      * parameter. An SSO ticket can also be presented by the user's web browser
117      * (in form of a cookie). The method will try to perform an SSO
118      * authentication if the conditions for this is met, else it will present
119      * the login page to the user.
120      * @param request
121      *            The HTTP request object.
122      * @param response
123      *            The HTTP response object.
124      * @throws IOException
125      *             Required by interface.
126      * @throws ServletException
127      *             Required by interface.
128      */
129 
130     // TODO: Elaborate the JavaDoc, with references to RequestUtil and required
131     // parameters.
132     public void doGet(final HttpServletRequest request,
133                       final HttpServletResponse response)
134     throws IOException, ServletException {
135 
136         // Get current configuration.
137         final Properties config = getConfig();
138 
139         // Public computer - should we deny use of SSO?
140         final String denySSOChoice = RequestUtil.getCookieValue(config.getProperty(RequestUtil.PROP_COOKIE_DENYSSO), request.getCookies());
141         final boolean denySSO;
142         if (denySSOChoice == null || denySSOChoice.equals("false") || denySSOChoice.equals("")) {
143 
144             // Allow SSO.
145             request.setAttribute(RequestUtil.ATTR_SELECTED_DENYSSO, new Boolean(false));
146             denySSO = false;
147 
148         } else {
149 
150             // Deny SSO.
151             request.setAttribute(RequestUtil.ATTR_SELECTED_DENYSSO, new Boolean(true));
152             denySSO = true;
153 
154         }
155 
156         // Attempt SSO.
157         if (!denySSO) {
158             final String serviceTicket;
159             try {
160 
161                 // Get SSO cookie name.
162                 final String ssoTicketId = RequestUtil.getCookieValue(config.getProperty(RequestUtil.PROP_COOKIE_SSO), request.getCookies());
163 
164                 if (ssoTicketId != null) {
165                     serviceTicket = MoriaController.attemptSingleSignOn(request.getParameter(config.getProperty(RequestUtil.PROP_LOGIN_TICKET_PARAM)), ssoTicketId);
166 
167                     // Redirect back to web service.
168                     response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
169                     response.setHeader("Location", MoriaController.getRedirectURL(serviceTicket));
170 
171                     // SSO succeeded, we're finished.
172                     return;
173                 }
174             } catch (UnknownTicketException e) {
175                 // SSO failed; continue with normal authentication.
176             } catch (MoriaControllerException e) {
177                 // Do not handle this exception here. Will be handled by the
178                 // showLoginPage method.
179             } catch (UnknownServicePrincipalException e) {
180                 // TODO: Resolve handling.
181             }
182         }
183 
184         // Display login page.
185         showLoginPage(request, response, null);
186     }
187 
188 
189     /***
190      * Handles the POST request. The POST request indicates that the user is
191      * trying to authenticate. If the authentication is successful, the user is
192      * redirected back to the originating web service, else the user is
193      * presented with an error message.
194      * @param request
195      *            The HTTP request.
196      * @param response
197      *            The HTTP response.
198      * @throws IOException
199      *             Required by interface.
200      * @throws ServletException
201      *             Required by interface.
202      */
203     public void doPost(final HttpServletRequest request,
204                        final HttpServletResponse response)
205     throws IOException, ServletException {
206 
207         // Get current configuration.
208         final Properties config = getConfig();
209 
210         // Get login and SSO ticket ID.
211         final String loginTicketId = request.getParameter(config.getProperty(RequestUtil.PROP_LOGIN_TICKET_PARAM));
212         final String ssoTicketId = request.getParameter(config.getProperty(RequestUtil.PROP_COOKIE_SSO));
213 
214         // Get user's credentials and organization (organization from dropdown).
215         final String username = request.getParameter(RequestUtil.PARAM_USERNAME);
216         final String password = request.getParameter(RequestUtil.PARAM_PASSWORD);
217         String org = request.getParameter(RequestUtil.PARAM_ORG);
218 
219         // Did the user choose to deny SSO?
220         String denySSOStr = request.getParameter(RequestUtil.PARAM_DENYSSO);
221         final boolean denySSO = (denySSOStr != null && denySSOStr.equals("true"));
222         if (denySSO)
223             denySSOStr = "true";
224         else
225             denySSOStr = "false";
226 
227         // Set cookie to remember user's SSO choice.
228         final Cookie denySSOCookie = RequestUtil.createCookie((String) config.get(RequestUtil.PROP_COOKIE_DENYSSO), denySSOStr, new Integer((String) config.get(RequestUtil.PROP_COOKIE_DENYSSO_TTL)).intValue());
229         response.addCookie(denySSOCookie);
230         request.setAttribute(RequestUtil.ATTR_SELECTED_DENYSSO, new Boolean(denySSO));
231 
232         // Parse organization, if set in username, and validate results.
233         if (username.indexOf("@") != -1)
234             org = username.substring(username.indexOf("@") + 1, username.length());
235         if (org == null || org.equals("") || org.equals("null")) {
236             showLoginPage(request, response, RequestUtil.ERROR_NO_ORG);
237             return;
238         } else if (!RequestUtil.parseConfig(getConfig(), RequestUtil.PROP_ORG, (String) config.get(RequestUtil.PROP_LOGIN_DEFAULT_LANGUAGE)).containsValue(org)) {
239             showLoginPage(request, response, RequestUtil.ERROR_INVALID_ORG);
240             return;
241         }
242 
243         // Store user's organization selection in cookie.
244         final Cookie orgCookie = RequestUtil.createCookie((String) config.get(RequestUtil.PROP_COOKIE_ORG), request.getParameter(RequestUtil.PARAM_ORG), new Integer((String) config.get(RequestUtil.PROP_COOKIE_ORG_TTL)).intValue());
245         response.addCookie(orgCookie);
246 
247         /* Attempt login */
248         final Map tickets;
249         final String redirectURL;
250         try {
251             tickets = MoriaController.attemptLogin(loginTicketId, ssoTicketId, username + "@" + org, password, denySSO);
252             redirectURL = MoriaController.getRedirectURL((String) tickets.get(MoriaController.SERVICE_TICKET));
253         } catch (AuthenticationException e) {
254             showLoginPage(request, response, RequestUtil.ERROR_AUTHENTICATION_FAILED);
255             return;
256         } catch (UnknownTicketException e) {
257             showLoginPage(request, response, RequestUtil.ERROR_UNKNOWN_TICKET);
258             return;
259         } catch (DirectoryUnavailableException e) {
260             showLoginPage(request, response, RequestUtil.ERROR_DIRECTORY_DOWN);
261             return;
262         } catch (InoperableStateException e) {
263             showLoginPage(request, response, RequestUtil.ERROR_MORIA_DOWN);
264             return;
265         } catch (IllegalInputException e) {
266             showLoginPage(request, response, RequestUtil.ERROR_NO_CREDENTIALS);
267             return;
268         } catch (AuthorizationException e) {
269             showLoginPage(request, response, RequestUtil.ERROR_AUTHORIZATION_FAILED);
270             return;
271         }
272 
273         // Authentication is now complete.
274 
275         // If we didn't disallow SSO, then store the SSO ticket in a cookie.
276         if (!denySSO) {
277             final Cookie ssoTicketCookie = RequestUtil.createCookie((String) config.get(RequestUtil.PROP_COOKIE_SSO), (String) tickets.get(MoriaController.SSO_TICKET), new Integer((String) config.get(RequestUtil.PROP_COOKIE_SSO_TTL)).intValue());
278             response.addCookie(ssoTicketCookie);
279         }
280 
281         // Redirect back to the web service.
282         response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
283         response.setHeader("Location", redirectURL);
284     }
285 
286 
287     /***
288      * Displays the login page. The method fills the request object with values
289      * and then passes the request to the JSP.
290      * @param request
291      *            The HTTP request.
292      * @param response
293      *            The HTTP response.
294      * @param errorType
295      *            The type of the error set by the caller.
296      * @throws IOException
297      *             When the JSP throws this exception.
298      * @throws ServletException
299      *             When the JSP throws this exception.
300      */
301     // TODO: Elaborate JavaDoc for this method.
302     private void showLoginPage(final HttpServletRequest request,
303                                final HttpServletResponse response,
304                                String errorType)
305     throws IOException, ServletException {
306         
307         /*
308          * Preparations requiring module configuration only:
309          */ 
310 
311         // Get configuration.
312         final Properties config = getConfig();
313         
314         // Get the login ticket.
315         final String loginTicket = request.getParameter(config.getProperty(RequestUtil.PROP_LOGIN_TICKET_PARAM));
316         
317         // Set the base authentication page URL.
318         request.setAttribute(RequestUtil.ATTR_BASE_URL, config.getProperty(RequestUtil.PROP_LOGIN_URL_PREFIX) + "?" + config.getProperty(RequestUtil.PROP_LOGIN_TICKET_PARAM) + "=" + loginTicket);
319         
320         // Set link to the FAQ.
321         request.setAttribute("faqlink", config.get(RequestUtil.FAQ_LINK));
322         
323         // Set the user's language cookie, if language was changed.
324         if (request.getParameter(RequestUtil.PARAM_LANG) != null)
325             response.addCookie(RequestUtil.createCookie((String) config.get(RequestUtil.PROP_COOKIE_LANG), request.getParameter(RequestUtil.PARAM_LANG), new Integer((String) config.get(RequestUtil.PROP_COOKIE_LANG_TTL)).intValue()));
326 
327         /*
328          * Preparations also requiring service properties. 
329          */
330         
331         // Get service properties and set security level.
332         HashMap serviceProperties = null;
333         try {
334 
335             serviceProperties = MoriaController.getServiceProperties(loginTicket);
336             request.setAttribute(RequestUtil.ATTR_SEC_LEVEL, "" + MoriaController.getSecLevel(loginTicket));
337 
338             // TODO: Move error handling into doGet instead, where
339             // MoriaControllerException is ignored.
340         } catch (UnknownTicketException e) {
341             errorType = RequestUtil.ERROR_UNKNOWN_TICKET;
342         } catch (IllegalInputException e) {
343             errorType = RequestUtil.ERROR_UNKNOWN_TICKET;
344         } catch (InoperableStateException e) {
345             errorType = RequestUtil.ERROR_MORIA_DOWN;
346         }
347 
348         // Set service display name and URL.
349         if (serviceProperties != null) {
350             request.setAttribute(RequestUtil.ATTR_CLIENT_NAME, serviceProperties.get(RequestUtil.CONFIG_DISPLAY_NAME));
351             request.setAttribute(RequestUtil.ATTR_CLIENT_URL, serviceProperties.get(RequestUtil.CONFIG_URL));
352         }
353         
354         /*
355          * Preparations also requiring user cookies.
356          */
357         
358         // Get user cookies.
359         final Cookie[] userCookies = request.getCookies();
360 
361         // Can we get page language from user's cookie?
362         String cookieLanguage = null;
363         if (userCookies != null)
364             cookieLanguage = RequestUtil.getCookieValue((String) config.get(RequestUtil.PROP_COOKIE_LANG), userCookies);
365 
366         // Can we get page language from service defaults?
367         String serviceLanguage = null;
368         if (serviceProperties != null)
369             serviceLanguage = (String) serviceProperties.get(RequestUtil.CONFIG_LANG);
370 
371         // Set page language.
372         final ResourceBundle bundle = RequestUtil.getBundle(RequestUtil.BUNDLE_LOGIN, request.getParameter(RequestUtil.PARAM_LANG), cookieLanguage, serviceLanguage, request.getHeader("Accept-Language"), (String) config.get(RequestUtil.PROP_LOGIN_DEFAULT_LANGUAGE));
373         request.setAttribute(RequestUtil.ATTR_BUNDLE, bundle);
374         request.setAttribute(RequestUtil.ATTR_LANGUAGES, RequestUtil.parseConfig(getConfig(), RequestUtil.PROP_LANGUAGE, RequestUtil.PROP_COMMON));
375         request.setAttribute(RequestUtil.ATTR_SELECTED_LANG, bundle.getLocale());
376 
377         /*
378          * Preparations also requiring service principal. 
379          */
380         
381         // Can we get the service principal?
382         String servicePrincipal = null;
383         if (serviceProperties != null)
384             servicePrincipal = (String) serviceProperties.get(RequestUtil.CONFIG_SERVICE_PRINCIPAL);
385 
386         // Resolve list of allowed organizations for this service principal.
387         TreeMap allowedOrganizations = null;
388         if (servicePrincipal != null) {
389             
390             // Remove illegal organizations for this (known) service.
391             allowedOrganizations = RequestUtil.parseConfig(getConfig(), RequestUtil.PROP_ORG, bundle.getLocale().getLanguage());
392             Iterator i = allowedOrganizations.values().iterator();
393             while (i.hasNext()) {
394                 try {
395                     if (!MoriaController.isOrganizationAllowedForService(servicePrincipal, (String) i.next()))
396                         i.remove();
397                 } catch (UnknownServicePrincipalException e) {
398                     // This is ignored; an unknown service principal should
399                     // never even reach this stage.
400                     log.logWarn("Unexpected service principal: " + servicePrincipal);
401                 }
402             }
403         }
404         request.setAttribute(RequestUtil.ATTR_ORGANIZATIONS, allowedOrganizations);
405         
406         // Get the requested attributes
407         try {
408             String [] attrs = MoriaController.getRequestedAttributes(loginTicket, servicePrincipal);
409             request.setAttribute(RequestUtil.ATTR_REQUESTED_ATTRIBUTES, attrs);
410        } catch (Exception e) {
411             log.logWarn("Unable to get the requested attributes: " + servicePrincipal);
412         }
413         String showAttrs = request.getParameter(RequestUtil.PARAM_SHOW_ATTRS);
414         String show = showAttrs != null ? showAttrs : "false";
415         request.setAttribute(RequestUtil.PARAM_SHOW_ATTRS, show);
416         
417         // Get default organization from service configuration, if possible.
418         String defaultOrganization = null;
419         if (serviceProperties != null)
420             defaultOrganization = (String) serviceProperties.get(RequestUtil.CONFIG_HOME);
421         
422         // Override with default organization from URL parameter.
423         String value = request.getParameter(RequestUtil.PARAM_ORG);
424         if (value != null)
425             defaultOrganization = value;
426         
427         // Override with default organization from user cookie.
428         value = RequestUtil.getCookieValue((String) config.get(RequestUtil.PROP_COOKIE_ORG), userCookies); 
429         if (value != null)
430             defaultOrganization = value;
431         
432         // Set default organization.
433         request.setAttribute(RequestUtil.ATTR_SELECTED_ORG, defaultOrganization);
434 
435         /*
436          * Finish preparations and show page.
437          */
438         
439         // Set error message, if any.
440         request.setAttribute(RequestUtil.ATTR_ERROR_TYPE, errorType);
441 
442         // Process JSP.
443         final RequestDispatcher rd = getServletContext().getNamedDispatcher("Login.JSP");
444         rd.forward(request, response);
445     }
446 
447 
448     /***
449      * Gets the current configuration from the context. The configuration is
450      * expected to be set by the controller before requests are sent to this
451      * servlet.
452      * @return The current configuration, as read from the servlet context.
453      * @throws IllegalStateException
454      *             If the configuration has not been set, or if any required
455      *             configuration parameters are missing.
456      * @see #REQUIRED_PARAMETERS
457      */
458     private Properties getConfig() {
459         return getServletConfig(getRequiredParameters(), log);
460     }
461 }