001: /*
002: * Copyright 2003 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package velosurf.web.auth;
018:
019: import java.io.IOException;
020: import java.util.Locale;
021:
022: import javax.servlet.Filter;
023: import javax.servlet.FilterChain;
024: import javax.servlet.FilterConfig;
025: import javax.servlet.ServletException;
026: import javax.servlet.ServletRequest;
027: import javax.servlet.ServletResponse;
028: import javax.servlet.http.HttpServletRequest;
029: import javax.servlet.http.HttpServletResponse;
030: import javax.servlet.http.HttpSession;
031:
032: import velosurf.util.*;
033: import velosurf.web.l10n.Localizer;
034:
035: /**
036: * <p>This class is a servlet filter used to protect web pages behind an authentication mechanism. When a
037: * non-authenticated user requests a private page, (s)he is redirected towards the login page and thereafter,
038: * if (s)he loggued in successfully, towards his(her) initially requested page.</p>
039: *
040: * <p>Authentication is performed via a CRAM (challenge-response authentication mechanism).
041: * Passwords are encrypted using the method given as parameter to the Authenticator tool in toolbox.xml. The provided
042: * Javascript file /src/javascript/md5.js implements the HmacMD5 method on the client side.</p>
043: *
044: * <p>This filter works in conjunction with an Authenticator object that must be present in the session scope
045: * of the toolbox and with a javascript password encryption function.</p>
046: *
047: * <p>To use it, you just have to map private urls (and especially, the target of the login form, this is
048: * very important for the authentication to work properly!) to go through this filter, as in :</p>
049: * <xmp>
050: * <filter>
051: * <filter-name>authentication</filter-name>
052: * <filter-class>auth.AuthenticationFilter</filter-class>
053: * </filter>
054: * <filter-mapping>
055: * <filter-name>authentication</filter-name>
056: * <url-pattern>/auth/*</url-pattern>
057: * </filter-mapping>
058: * </xmp>
059: *
060: * <p>The password is encrypted in an irreversible manner into an <i>answer</i>, and to check the login,
061: * the answer that the client sends back to the server is compared to the correct awaited answer.</p>
062: *
063: * <p>The javascript file <i>login.js.vtl</i> contains the necessary encryption functions. It uses
064: * the <i>bignum.js</i> library file. You will find those files in <code>/src/resources/auth</code>
065: * or in the auth-l10n sample webapp.</p>
066: *
067: * <p>The filter expect the login to be present in the HTTP 'login' form field, and the answer in
068: * the 'answer' form field (which should be all right if you use the login.js.vtl as is). The action of the form
069: * is never used (since the filter will redirect the user towards the page asked before the login), but <b>it must
070: * be catched by an url-pattern of this filter</b>. You can for instance define a mapping towards "/process_login".</p>
071: *
072: * <p>The loggued state is materialized by the presence of a user Object in the session under
073: * the <i>user</i> key. This user object in the one returned by the abstract method Authenticator.getUser(login).</p>
074: *
075: * <p>This filter will search for an occurrence of a localizer tool in the session toolbox to resolve some values.
076: * The presence of this localizer is optional.</p>
077: *
078: * <p>Optional configuration parameters:
079: * <ul>
080: * <li><code>login-field</code>: name of the login form field.</li>
081: * <li><code>password-field</code>: name of the password field.</li>
082: * <li><code>max-inactive</code>: delay upon which an inactive user is disconnected in seconds.
083: * The default value is one hour.</li>
084: * <li><code>login-page</code>: the login page URI. The "<code>@</code>" pattern applies as well. Default is '/login.html'.</li>
085: * <li><code>authenticated-index-page</code>: the default page once authenticated. The "<code>@</code>" pattern applies as well.
086: * Default is '/loggued.html'.</li>
087: * <li><code>bad-login-message</code>: the message to be displayed in case of bad login. If this parameter is not
088: * specified, the filter will try to get a reference from the localizer tool and ask it for a "badLogin"
089: * message, and if this fails, it will simply use "Bad login or password.".</li>
090: * <li><code>disconnected-message</code>: the message to be displayed when the user is disconnected after a period
091: * of inactivity on the site. Same remark if this parameter is not supplied: the filter will search
092: * for a "disconnected" message in the localizer tool if present, and otherwise display "You have been disconnected."</li>
093: * </ul>
094: * </p>
095: *
096: *
097: * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
098: *
099: */
100:
101: public class AuthenticationFilter implements Filter {
102:
103: /** filter config. */
104: private FilterConfig config = null;
105:
106: /** Max inactive interval. */
107: private int maxInactive = 3600;
108:
109: /** Login field. */
110: private String loginField = "login";
111:
112: /** Password field. */
113: private String passwordField = "password";
114:
115: /** Login page. */
116: private String loginPage = "/login.html.vtl";
117:
118: /** Index of the authenticated zone. */
119: private String authenticatedIndexPage = "/index.html.vtl";
120:
121: /** Message in case of bad login. */
122: private String badLoginMessage = null;
123:
124: /** Message key in case of bad login. */
125: private String badLoginMsgKey = "badLogin";
126:
127: /** Default bad login message. */
128: private static final String defaultBadLoginMessage = "Bad login or password.";
129:
130: /** Message in case of disconnection. */
131: private String disconnectedMessage = null;
132:
133: /** Message key in case of disconnection. */
134: private String disconnectedMsgKey = "disconnected";
135:
136: /** Default message in case of disconnection. */
137: private static final String defaultDisconnectedMessage = "You have been disconnected.";
138:
139: /**
140: * Initialization.
141: * @param config filter config
142: * @throws ServletException
143: */
144: public void init(FilterConfig config) throws ServletException {
145: this .config = config;
146:
147: /* logger initialization */
148: if (!Logger.isInitialized()) {
149: Logger.setWriter(new ServletLogWriter(config
150: .getServletContext()));
151: }
152:
153: /* max-inactive */
154: String param = this .config.getInitParameter("max-inactive");
155: if (param != null) {
156: try {
157: maxInactive = Integer.parseInt(param);
158: } catch (NumberFormatException nfe) {
159: Logger
160: .error("AuthenticationFilter: bad format for the max-inactive parameter: "
161: + param);
162: }
163: }
164:
165: /* login field */
166: param = this .config.getInitParameter("login-field");
167: if (param != null) {
168: loginField = param;
169: }
170:
171: /* password field */
172: param = this .config.getInitParameter("password-field");
173: if (param != null) {
174: passwordField = param;
175: }
176:
177: /* login page */
178: param = this .config.getInitParameter("login-page");
179: if (param != null) {
180: loginPage = param;
181: }
182: /* authenticated index page */
183: param = this .config
184: .getInitParameter("authenticated-index-page");
185: if (param != null) {
186: authenticatedIndexPage = param;
187: }
188: /* bad login message */
189: badLoginMessage = this .config
190: .getInitParameter("bad-login-message");
191: /* disconnected message */
192: disconnectedMessage = this .config
193: .getInitParameter("disconnected-message");
194: }
195:
196: /**
197: * Filtering.
198: * @param servletRequest request
199: * @param servletResponse response
200: * @param chain filter chain
201: * @throws IOException
202: * @throws ServletException
203: */
204: public void doFilter(ServletRequest servletRequest,
205: ServletResponse servletResponse, FilterChain chain)
206: throws IOException, ServletException {
207: HttpServletRequest request = (HttpServletRequest) servletRequest;
208: HttpSession session = request.getSession(false);
209: HttpServletResponse response = (HttpServletResponse) servletResponse;
210:
211: String uri = request.getRequestURI();
212:
213: String login = null, password = null;
214:
215: if (session != null
216: && session.getId().equals(
217: request.getRequestedSessionId()) /* not needed in theory */
218: && session.getAttribute("velosurf.auth.user") != null) {
219: /* already loggued*/
220:
221: /* if asked to logout, well, logout! */
222: if (uri.endsWith("/logout.do")) {
223: doLogout(request, response, chain);
224: } else {
225: doProcessAuthentified(request, response, chain);
226: }
227: } else {
228: /* never protect the login page itself */
229: if (uri.equals(resolveLocalizedUri(request, loginPage))) {
230: chain.doFilter(request, response);
231: return;
232: }
233: if (session == null) {
234: session = request.getSession(true);
235: } else {
236: /* clear any previous loginMessage */
237: session.removeAttribute("loginMessage");
238: }
239: session.removeAttribute("velosurf.auth.user");
240: if (uri.endsWith("/login.do")
241: && (login = request.getParameter(loginField)) != null
242: && (password = request.getParameter(passwordField)) != null
243: && session.getId().equals(
244: request.getRequestedSessionId())) {
245: // a user is trying to log in
246:
247: // get a reference to the authenticator tool
248: BaseAuthenticator auth = ToolFinder.findSessionTool(
249: session, BaseAuthenticator.class);
250:
251: if (auth == null) {
252: Logger
253: .error("auth: cannot find any reference to the authenticator tool in the session!");
254: /* Maybe the current user tried to validate an expired login form... well... ask him again... */
255: response.sendRedirect(resolveLocalizedUri(request,
256: loginPage));
257: return;
258: }
259: // check answer
260: if (auth.checkLogin(login, password)) {
261: // login ok
262: doLogin(request, response, chain);
263: } else {
264: badLogin(request, response, chain);
265: }
266: } else {
267: /* do not redirect to the logout */
268: if (uri.endsWith("/logout.do")) {
269: response.sendRedirect(resolveLocalizedUri(request,
270: loginPage));
271: } else {
272: doRedirect(request, response, chain);
273: }
274: }
275: }
276: }
277:
278: protected void doRedirect(HttpServletRequest request,
279: HttpServletResponse response, FilterChain chain)
280: throws IOException, ServletException {
281: /* save the original request */
282: String uri = request.getRequestURI();
283: Logger.trace("auth: saving request towards " + uri);
284: HttpSession session = request.getSession();
285: session.setAttribute("velosurf.auth.saved-request",
286: SavedRequest.saveRequest(request));
287:
288: /* check to see if the current user has been disconnected
289: note that this test will fail when the servlet container
290: reuses session ids */
291: boolean disconnected = false;
292: String reqId = request.getRequestedSessionId();
293: if (reqId != null
294: && (session == null || !session.getId().equals(reqId))) {
295: disconnected = true;
296: }
297:
298: if (disconnected) {
299: String message = disconnectedMessage != null ? disconnectedMessage
300: : getMessage(ToolFinder.findSessionTool(session,
301: Localizer.class), disconnectedMsgKey,
302: defaultDisconnectedMessage);
303: session.setAttribute("loginMessage", message);
304: }
305: // redirect to login page
306: String loginPage = resolveLocalizedUri(request, this .loginPage);
307: Logger.trace("auth: redirecting unauthenticated user to "
308: + loginPage);
309: response.sendRedirect(loginPage);
310: }
311:
312: protected void doLogin(HttpServletRequest request,
313: HttpServletResponse response, FilterChain chain)
314: throws IOException, ServletException {
315: String login = request.getParameter(loginField);
316: Logger.info("auth: user '" + login
317: + "' successfully loggued in.");
318: HttpSession session = request.getSession();
319: session.setAttribute("velosurf.auth.user", ToolFinder
320: .findSessionTool(session, BaseAuthenticator.class)
321: .getUser(login));
322: if (maxInactive > 0) {
323: Logger
324: .trace("auth: setting session max inactive interval to "
325: + maxInactive);
326: session.setMaxInactiveInterval(maxInactive);
327: }
328: session.removeAttribute("challenge");
329: session.removeAttribute("authenticator");
330: // then handle the former request if not null
331: goodLogin(request, response, chain);
332: }
333:
334: protected void goodLogin(HttpServletRequest request,
335: HttpServletResponse response, FilterChain chain)
336: throws IOException, ServletException {
337: HttpSession session = request.getSession();
338: SavedRequest savedRequest = (SavedRequest) session
339: .getAttribute("velosurf.auth.saved-request");
340: if (savedRequest == null) {
341: // redirect to /auth/index.html
342: String authenticatedIndexPage = resolveLocalizedUri(
343: request, this .authenticatedIndexPage);
344: Logger.trace("auth: redirecting newly loggued user to "
345: + authenticatedIndexPage);
346: response.sendRedirect(authenticatedIndexPage);
347: } else {
348: session.removeAttribute("velosurf.auth.saved-request");
349: String formerUrl = savedRequest.getRequestURI();
350: String query = savedRequest.getQueryString();
351: query = (query == null ? "" : "?" + query);
352: formerUrl += query;
353: Logger.trace("auth: redirecting newly loggued user to "
354: + formerUrl);
355: response.sendRedirect(formerUrl);
356: }
357: }
358:
359: protected void badLogin(HttpServletRequest request,
360: HttpServletResponse response, FilterChain chain)
361: throws IOException, ServletException {
362: Logger.warn("auth: user " + request.getParameter(loginField)
363: + " made an unsuccessfull login attempt.");
364: HttpSession session = request.getSession();
365: String message = badLoginMessage != null ? badLoginMessage
366: : getMessage(ToolFinder.findSessionTool(session,
367: Localizer.class), badLoginMsgKey,
368: defaultBadLoginMessage);
369: session.setAttribute("loginMessage", message);
370: // redirect to login page
371: String loginPage = resolveLocalizedUri(request, this .loginPage);
372: Logger.trace("auth: redirecting unauthenticated user to "
373: + loginPage);
374: response.sendRedirect(loginPage);
375: }
376:
377: protected void doProcessAuthentified(HttpServletRequest request,
378: HttpServletResponse response, FilterChain chain)
379: throws IOException, ServletException {
380: /* if the request is still pointing on /login.html, redirect to /auth/index.html */
381: String uri = request.getRequestURI();
382: HttpSession session = request.getSession();
383: if (uri.equals(resolveLocalizedUri(request, loginPage))) {
384: String authenticatedIndexPage = resolveLocalizedUri(
385: request, this .authenticatedIndexPage);
386: Logger.trace("auth: redirecting loggued user to "
387: + authenticatedIndexPage);
388: response.sendRedirect(authenticatedIndexPage);
389: } else {
390: Logger.trace("auth: user is authenticated.");
391: SavedRequest saved = (SavedRequest) session
392: .getAttribute("velosurf.auth.saved-request");
393: if (saved != null
394: && saved.getRequestURL().equals(
395: request.getRequestURL())) {
396: session.removeAttribute("velosurf.auth.saved-request");
397: chain.doFilter(new SavedRequestWrapper(request, saved),
398: response);
399: } else {
400: chain.doFilter(request, response);
401: }
402: }
403: }
404:
405: protected void doLogout(HttpServletRequest request,
406: HttpServletResponse response, FilterChain chain)
407: throws IOException, ServletException {
408: HttpSession session = request.getSession();
409: Logger.trace("auth: user logged out");
410: session.removeAttribute("velosurf.auth.user");
411: String loginPage = resolveLocalizedUri(request, this .loginPage);
412: response.sendRedirect(loginPage);
413: }
414:
415: protected String resolveLocalizedUri(HttpServletRequest request,
416: String uri) {
417: if (uri.indexOf('@') != -1) {
418: /* means the uri need the current locale */
419: Locale locale = null;
420: HttpSession session = request.getSession();
421: if (session != null) {
422: locale = (Locale) session
423: .getAttribute("velosurf.l10n.active-locale"); /* TODO: gather 'active-locale' handling in HTTPLocalizerTool */
424: }
425:
426: if (locale == null) {
427: Logger
428: .error("auth: cannot find the active locale in the session!");
429: Logger
430: .error("auth: the LocalizationFilter must reside before this filter in the filters chain.");
431: //response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
432: return uri;
433: }
434: uri = uri.replaceAll("@", locale.toString());
435: }
436: return uri;
437: }
438:
439: protected String getAuthenticatedIndexPage() {
440: return authenticatedIndexPage;
441: }
442:
443: /**
444: * Message getter.
445: * @param localizer localizer
446: * @param key key
447: * @param defaultMessage default message
448: * @return localized message or default message
449: */
450: protected String getMessage(Localizer localizer, String key,
451: String defaultMessage) {
452: String message = null;
453: if (localizer != null) {
454: message = localizer.get(key);
455: }
456: return message == null || message.equals(key) ? defaultMessage
457: : message;
458: }
459:
460: /**
461: * Destroy the filter.
462: */
463: public void destroy() {
464: }
465: }
|