/*
* Copyright 2003 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package velosurf.web.auth;
import java.io.IOException;
import java.util.Locale;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import velosurf.util.*;
import velosurf.web.l10n.Localizer;
/**
* <p>This class is a servlet filter used to protect web pages behind an authentication mechanism. When a
* non-authenticated user requests a private page, (s)he is redirected towards the login page and thereafter,
* if (s)he loggued in successfully, towards his(her) initially requested page.</p>
*
* <p>Authentication is performed via a CRAM (challenge-response authentication mechanism).
* Passwords are encrypted using the method given as parameter to the Authenticator tool in toolbox.xml. The provided
* Javascript file /src/javascript/md5.js implements the HmacMD5 method on the client side.</p>
*
* <p>This filter works in conjunction with an Authenticator object that must be present in the session scope
* of the toolbox and with a javascript password encryption function.</p>
*
* <p>To use it, you just have to map private urls (and especially, the target of the login form, this is
* very important for the authentication to work properly!) to go through this filter, as in :</p>
* <xmp>
* <filter>
* <filter-name>authentication</filter-name>
* <filter-class>auth.AuthenticationFilter</filter-class>
* </filter>
* <filter-mapping>
* <filter-name>authentication</filter-name>
* <url-pattern>/auth/*</url-pattern>
* </filter-mapping>
* </xmp>
*
* <p>The password is encrypted in an irreversible manner into an <i>answer</i>, and to check the login,
* the answer that the client sends back to the server is compared to the correct awaited answer.</p>
*
* <p>The javascript file <i>login.js.vtl</i> contains the necessary encryption functions. It uses
* the <i>bignum.js</i> library file. You will find those files in <code>/src/resources/auth</code>
* or in the auth-l10n sample webapp.</p>
*
* <p>The filter expect the login to be present in the HTTP 'login' form field, and the answer in
* the 'answer' form field (which should be all right if you use the login.js.vtl as is). The action of the form
* is never used (since the filter will redirect the user towards the page asked before the login), but <b>it must
* be catched by an url-pattern of this filter</b>. You can for instance define a mapping towards "/process_login".</p>
*
* <p>The loggued state is materialized by the presence of a user Object in the session under
* the <i>user</i> key. This user object in the one returned by the abstract method Authenticator.getUser(login).</p>
*
* <p>This filter will search for an occurrence of a localizer tool in the session toolbox to resolve some values.
* The presence of this localizer is optional.</p>
*
* <p>Optional configuration parameters:
* <ul>
* <li><code>login-field</code>: name of the login form field.</li>
* <li><code>password-field</code>: name of the password field.</li>
* <li><code>max-inactive</code>: delay upon which an inactive user is disconnected in seconds.
* The default value is one hour.</li>
* <li><code>login-page</code>: the login page URI. The "<code>@</code>" pattern applies as well. Default is '/login.html'.</li>
* <li><code>authenticated-index-page</code>: the default page once authenticated. The "<code>@</code>" pattern applies as well.
* Default is '/loggued.html'.</li>
* <li><code>bad-login-message</code>: the message to be displayed in case of bad login. If this parameter is not
* specified, the filter will try to get a reference from the localizer tool and ask it for a "badLogin"
* message, and if this fails, it will simply use "Bad login or password.".</li>
* <li><code>disconnected-message</code>: the message to be displayed when the user is disconnected after a period
* of inactivity on the site. Same remark if this parameter is not supplied: the filter will search
* for a "disconnected" message in the localizer tool if present, and otherwise display "You have been disconnected."</li>
* </ul>
* </p>
*
*
* @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
*
*/
public class AuthenticationFilter implements Filter {
/** filter config. */
private FilterConfig config = null;
/** Max inactive interval. */
private int maxInactive = 3600;
/** Login field. */
private String loginField = "login";
/** Password field. */
private String passwordField = "password";
/** Login page. */
private String loginPage = "/login.html.vtl";
/** Index of the authenticated zone. */
private String authenticatedIndexPage = "/index.html.vtl";
/** Message in case of bad login. */
private String badLoginMessage = null;
/** Message key in case of bad login. */
private String badLoginMsgKey = "badLogin";
/** Default bad login message. */
private static final String defaultBadLoginMessage = "Bad login or password.";
/** Message in case of disconnection. */
private String disconnectedMessage = null;
/** Message key in case of disconnection. */
private String disconnectedMsgKey = "disconnected";
/** Default message in case of disconnection. */
private static final String defaultDisconnectedMessage = "You have been disconnected.";
/**
* Initialization.
* @param config filter config
* @throws ServletException
*/
public void init(FilterConfig config) throws ServletException {
this.config = config;
/* logger initialization */
if (!Logger.isInitialized()) {
Logger.setWriter(new ServletLogWriter(config.getServletContext()));
}
/* max-inactive */
String param = this.config.getInitParameter("max-inactive");
if (param != null) {
try {
maxInactive = Integer.parseInt(param);
} catch (NumberFormatException nfe) {
Logger.error("AuthenticationFilter: bad format for the max-inactive parameter: "+param);
}
}
/* login field */
param = this.config.getInitParameter("login-field");
if (param != null) {
loginField = param;
}
/* password field */
param = this.config.getInitParameter("password-field");
if (param != null) {
passwordField = param;
}
/* login page */
param = this.config.getInitParameter("login-page");
if (param != null) {
loginPage = param;
}
/* authenticated index page */
param = this.config.getInitParameter("authenticated-index-page");
if (param != null) {
authenticatedIndexPage = param;
}
/* bad login message */
badLoginMessage = this.config.getInitParameter("bad-login-message");
/* disconnected message */
disconnectedMessage = this.config.getInitParameter("disconnected-message");
}
/**
* Filtering.
* @param servletRequest request
* @param servletResponse response
* @param chain filter chain
* @throws IOException
* @throws ServletException
*/
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpSession session = request.getSession(false);
HttpServletResponse response = (HttpServletResponse)servletResponse;
String uri = request.getRequestURI();
String login = null, password = null;
if (session != null
&& session.getId().equals(request.getRequestedSessionId()) /* not needed in theory */
&& session.getAttribute("velosurf.auth.user") != null) {
/* already loggued*/
/* if asked to logout, well, logout! */
if (uri.endsWith("/logout.do")) {
doLogout(request,response,chain);
} else {
doProcessAuthentified(request,response,chain);
}
} else {
/* never protect the login page itself */
if (uri.equals(resolveLocalizedUri(request,loginPage))) {
chain.doFilter(request,response);
return;
}
if (session == null) {
session = request.getSession(true);
} else {
/* clear any previous loginMessage */
session.removeAttribute("loginMessage");
}
session.removeAttribute("velosurf.auth.user");
if ( uri.endsWith("/login.do")
&& (login = request.getParameter(loginField)) != null
&& (password = request.getParameter(passwordField)) != null
&& session.getId().equals(request.getRequestedSessionId())) {
// a user is trying to log in
// get a reference to the authenticator tool
BaseAuthenticator auth = ToolFinder.findSessionTool(session,BaseAuthenticator.class);
if (auth == null) {
Logger.error("auth: cannot find any reference to the authenticator tool in the session!");
/* Maybe the current user tried to validate an expired login form... well... ask him again... */
response.sendRedirect(resolveLocalizedUri(request,loginPage));
return;
}
// check answer
if (auth.checkLogin(login,password)) {
// login ok
doLogin(request,response,chain);
} else {
badLogin(request,response,chain);
}
} else {
/* do not redirect to the logout */
if (uri.endsWith("/logout.do")) {
response.sendRedirect(resolveLocalizedUri(request,loginPage));
} else {
doRedirect(request,response,chain);
}
}
}
}
protected void doRedirect(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws IOException, ServletException {
/* save the original request */
String uri = request.getRequestURI();
Logger.trace("auth: saving request towards "+uri);
HttpSession session = request.getSession();
session.setAttribute("velosurf.auth.saved-request",SavedRequest.saveRequest(request));
/* check to see if the current user has been disconnected
note that this test will fail when the servlet container
reuses session ids */
boolean disconnected = false;
String reqId = request.getRequestedSessionId();
if (reqId != null && (session == null || !session.getId().equals(reqId))) {
disconnected = true;
}
if(disconnected) {
String message = disconnectedMessage != null ?
disconnectedMessage :
getMessage(ToolFinder.findSessionTool(session,Localizer.class),disconnectedMsgKey,defaultDisconnectedMessage);
session.setAttribute("loginMessage",message);
}
// redirect to login page
String loginPage = resolveLocalizedUri(request,this.loginPage);
Logger.trace("auth: redirecting unauthenticated user to "+loginPage);
response.sendRedirect(loginPage);
}
protected void doLogin(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws IOException, ServletException {
String login = request.getParameter(loginField);
Logger.info("auth: user '"+login+"' successfully loggued in.");
HttpSession session = request.getSession();
session.setAttribute("velosurf.auth.user", ToolFinder.findSessionTool(session,BaseAuthenticator.class).getUser(login));
if (maxInactive > 0) {
Logger.trace("auth: setting session max inactive interval to "+maxInactive);
session.setMaxInactiveInterval(maxInactive);
}
session.removeAttribute("challenge");
session.removeAttribute("authenticator");
// then handle the former request if not null
goodLogin(request,response,chain);
}
protected void goodLogin(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws IOException, ServletException {
HttpSession session = request.getSession();
SavedRequest savedRequest = (SavedRequest)session.getAttribute("velosurf.auth.saved-request");
if (savedRequest == null) {
// redirect to /auth/index.html
String authenticatedIndexPage = resolveLocalizedUri(request,this.authenticatedIndexPage);
Logger.trace("auth: redirecting newly loggued user to "+authenticatedIndexPage);
response.sendRedirect(authenticatedIndexPage);
} else {
session.removeAttribute("velosurf.auth.saved-request");
String formerUrl = savedRequest.getRequestURI();
String query = savedRequest.getQueryString();
query = (query == null ? "" : "?"+query);
formerUrl += query;
Logger.trace("auth: redirecting newly loggued user to "+formerUrl);
response.sendRedirect(formerUrl);
}
}
protected void badLogin(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws IOException, ServletException {
Logger.warn("auth: user " + request.getParameter(loginField) + " made an unsuccessfull login attempt.");
HttpSession session = request.getSession();
String message = badLoginMessage != null ?
badLoginMessage :
getMessage(ToolFinder.findSessionTool(session,Localizer.class),badLoginMsgKey,defaultBadLoginMessage);
session.setAttribute("loginMessage",message);
// redirect to login page
String loginPage = resolveLocalizedUri(request,this.loginPage);
Logger.trace("auth: redirecting unauthenticated user to "+loginPage);
response.sendRedirect(loginPage);
}
protected void doProcessAuthentified(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws IOException, ServletException {
/* if the request is still pointing on /login.html, redirect to /auth/index.html */
String uri = request.getRequestURI();
HttpSession session = request.getSession();
if (uri.equals(resolveLocalizedUri(request,loginPage))) {
String authenticatedIndexPage = resolveLocalizedUri(request,this.authenticatedIndexPage);
Logger.trace("auth: redirecting loggued user to "+authenticatedIndexPage);
response.sendRedirect(authenticatedIndexPage);
} else {
Logger.trace("auth: user is authenticated.");
SavedRequest saved = (SavedRequest)session.getAttribute("velosurf.auth.saved-request");
if (saved != null && saved.getRequestURL().equals(request.getRequestURL())) {
session.removeAttribute("velosurf.auth.saved-request");
chain.doFilter(new SavedRequestWrapper(request,saved),response);
} else {
chain.doFilter(request,response);
}
}
}
protected void doLogout(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws IOException, ServletException {
HttpSession session = request.getSession();
Logger.trace("auth: user logged out");
session.removeAttribute("velosurf.auth.user");
String loginPage = resolveLocalizedUri(request,this.loginPage);
response.sendRedirect(loginPage);
}
protected String resolveLocalizedUri(HttpServletRequest request,String uri) {
if (uri.indexOf('@')!=-1) {
/* means the uri need the current locale */
Locale locale = null;
HttpSession session = request.getSession();
if (session != null) {
locale = (Locale)session.getAttribute("velosurf.l10n.active-locale"); /* TODO: gather 'active-locale' handling in HTTPLocalizerTool */
}
if (locale == null)
{
Logger.error("auth: cannot find the active locale in the session!");
Logger.error("auth: the LocalizationFilter must reside before this filter in the filters chain.");
//response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return uri;
}
uri = uri.replaceAll("@",locale.toString());
}
return uri;
}
protected String getAuthenticatedIndexPage() {
return authenticatedIndexPage;
}
/**
* Message getter.
* @param localizer localizer
* @param key key
* @param defaultMessage default message
* @return localized message or default message
*/
protected String getMessage(Localizer localizer,String key,String defaultMessage) {
String message = null;
if (localizer != null) {
message = localizer.get(key);
}
return message == null || message.equals(key) ? defaultMessage : message;
}
/**
* Destroy the filter.
*/
public void destroy() {
}
}
|