2012/04/20

How to secure a Struts 1.3 application

Welcome to my first tutorial!

In this tutorial I will show you how to secure a Struts 1.3.8 web application with servlet filters. Basic knowledge of Struts is required to fully understand this article. Numerous HelloWorld tutorials are available using search engines, such Google, and any of them should be completely understood before starting this manual.

Why using servlet filters? Because is the only way of implementing access restrictions to methods, actions, and jsp files without changing the code of those methods, actions or jsp. This way we can centralize all the security aspect of our application on a few resources (filters and web.xml). Another alternatives that centralize the security aspect of our web application are provided combining Struts with other frameworks such as Spring Security.

In this example, we want to separate our users in two main categories: administrators and normal users. Normal users will, obviously, access to the user panel while administrators will have access also to the administration panel. It is because of this that administrator users will have two roles: “admin” and “user”.

The plan is to allow any anonymous user to access only to the login action (or login jsp). Any other request has to be performed by an user with the correct rights to do so. In order to accomplish this we will separate the administrator’s actions and resources and place all of them in the “admin” directory. Same thing with the user’s action and resources that will live in the “user” directory. We will place a filter for each directory that will intercept all the request on these resources and check for the security credentials of the user making the request.


Let’s take a look into the User class:

User.java
package com.jml.tutorial.secure.model;

import java.util.Arrays;
import java.util.Collection;

public class User {
    private String username;
    private Collection<String> roles;

    public User(String username, String[] roles) {
        super();
        this.username = username;
        this.roles = Arrays.asList(roles);
    }

    public boolean hasRole(String role) {
        return roles.contains(role);
    }

    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }

    public String[] getRoles() {
        return (String[]) roles.toArray();
    }
    public void setRoles(String[] roles) {
        this.roles = Arrays.asList(roles);
    }
}


As we can see, each user has a collection of roles and a method to check if a specific role is owned by the user. When a user logs in, the User object will be stored in the session object:

LoginAction.java
package com.jml.tutorial.secure.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.Globals;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;

import com.jml.tutorial.secure.bussiness.LoginBO;
import com.jml.tutorial.secure.form.LoginForm;
import com.jml.tutorial.secure.model.User;

public class LoginAction extends BaseAction {
    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) {
        LoginForm loginForm = (LoginForm) form;
        LoginBO loginBO = new LoginBO();
        String username = loginForm.getUsername();
        if (loginBO.checkLogin(username, loginForm.getPassword())) {
            User user = loginBO.getUser(username);
            request.getSession().setAttribute("user", user);
            if (user.hasRole("admin"))
                return findForwardAdminSuccess(mapping);
            else
                return findForwardUserSuccess(mapping);
        }
        ActionErrors errors = new ActionErrors();
        errors.add(ActionErrors.GLOBAL_MESSAGE, new ActionMessage(
                "error.authorization.invalid"));
        request.setAttribute(Globals.ERROR_KEY, errors);
        return findForwardFailure(mapping);
    }
}


Now we can create an filter with the mission of intercepting all the requests to a private area of our application and check if the user logged has the permission to perform the request.

AuthorizationFilter.java
package com.jml.tutorial.secure.security;

import java.io.IOException;

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 org.apache.struts.Globals;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionMessage;

import com.jml.tutorial.secure.model.User;

public class AuthorizationFilter implements Filter {
    private String[] roleNames;
    private String onErrorUrl;  

    public void init(FilterConfig arg0) throws ServletException {
        String roles = arg0.getInitParameter("roles");
        if (roles == null || "".equals(roles))
          roleNames = new String[0];
        else {
          roles.trim();
          roleNames = roles.split(" ");
        }

        onErrorUrl = arg0.getInitParameter("onError");
        if (onErrorUrl == null || "".equals(onErrorUrl))
          onErrorUrl = "/index.jsp";
    }

    public void doFilter(ServletRequest arg0, ServletResponse arg1,
            FilterChain arg2) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) arg0;
        HttpServletResponse res = (HttpServletResponse) arg1;
        HttpSession session = req.getSession();

        User user = (User) session.getAttribute("user");

        ActionErrors errors = new ActionErrors();
        if (user == null)
          errors.add(ActionErrors.GLOBAL_MESSAGE,
            new ActionMessage("error.authentication.required"));
        else {
          boolean hasRole = false;
          for (int i=0; i < roleNames.length; i++) {
            if (user.hasRole(roleNames[i])) {
              hasRole = true;
              break;
            }
          }

          if (!hasRole)
            errors.add(ActionErrors.GLOBAL_MESSAGE,
              new ActionMessage(
                      "error.authorization.nopermission",
                      user.getUsername()));
        }

        if (errors.isEmpty())
            arg2.doFilter(arg0, arg1);
        else {
          req.setAttribute(Globals.ERROR_KEY, errors);
          req.getRequestDispatcher(onErrorUrl).forward(req, res);
        }
    }    

    public void destroy() {    }
}


Because AuthorizationFilter is a child of javax.servlet.Filter it has to implement the init(), doFilter() and destroy() methods. Inside init() we will process the initial parameters (defined at web.xml) and prepare them for future use. Two parameters are used by this filter: roles and onError. The roles parameter stores a list of role names separated by one space. The onError parameter stores a path to the jsp file to redirect the client browser in case of the user doing the the request has not the right credentials.

As you can see, doFilter() will take an object User from the session, if it can’t find an User, doFilter() will deny the petition. If an user has logged in the application, doFilter() will check if he has the right role to access the resource (or one of them). If not it will deny the petition with a specific answer.

Let’s see how the filter is set up in web.xml.

web.xml
<?xml version="1.0" encoding="UTF-8"?>

<web-app
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 xsi:schemaLocation="
   http://java.sun.com/xml/ns/javaee
   http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

 id="WebApp_ID" version="2.5">
 <display-name>HowTo Security</display-name>

 <filter>
     <filter-name>userAccessFilter</filter-name>
     <filter-class>
       com.jml.tutorial.secure.security.AuthorizationFilter
     </filter-class>

     <init-param>
       <param-name>roles</param-name>
       <param-value>user</param-value>
     </init-param>

     <init-param>
       <param-name>onError</param-name>
       <param-value>/login.jsp</param-value>
     </init-param>
 </filter>

 <filter>
     <filter-name>adminAccessFilter</filter-name>
     <filter-class>
       com.jml.tutorial.secure.security.AuthorizationFilter
     </filter-class>

     <init-param>
       <param-name>roles</param-name>
       <param-value>admin</param-value>
     </init-param>

     <init-param>
       <param-name>onError</param-name>
       <param-value>/login.jsp</param-value>
     </init-param>
 </filter>

 <filter-mapping>
     <filter-name>userAccessFilter</filter-name>
     <url-pattern>/user/*</url-pattern>
 </filter-mapping>

 <filter-mapping>
     <filter-name>adminAccessFilter</filter-name>
     <url-pattern>/admin/*</url-pattern>
 </filter-mapping>  

 <servlet>
   <servlet-name>action</servlet-name>
   <servlet-class>
       org.apache.struts.action.ActionServlet
   </servlet-class>

   <init-param>
       <param-name>config</param-name>
       <param-value>
        /WEB-INF/struts/struts-config.xml
       </param-value>
   </init-param>

   <load-on-startup>1</load-on-startup>
 </servlet>

 <servlet-mapping>
      <servlet-name>action</servlet-name>
      <url-pattern>*.do</url-pattern>
 </servlet-mapping>

 <welcome-file-list>
     <welcome-file>index.jsp</welcome-file>
 </welcome-file-list>
</web-app>


We’ve set up two different AuthorizationFilters, one will protect all files under the directory /admin/ and restrict their access to users who have the role admin. The other will do the same for all the files placed under /user/ requiring the user role to access them. If the authentication fails, user will be redirected to /login.jsp.

One alternative of implementing the role restriction is to add to the userAccessFilter another role as follows:

web.xml
...
<filter>
   <filter-name>userAccessFilter</filter-name>
   <filter-class>
     com.hgro.beta.security.AuthorizationFilter
   </filter-class>
   <init-param>
     <param-name>roles</param-name>
     <param-value>user admin</param-value>
   </init-param>
...


That way we won’t need to add various roles to an administrator user, who previously had both user and admin roles. Both approaches are valid, you should implement whichever is more appropriate to your project.

But how we display error information? As you can see, if AuthorizationFilter fails it will redirect the user to /login.jsp (as seen in web.xml) adding the errors to the request object (as seen in AuthorizationFilter.java). Errors will be displayed using a message resource bundle through a key.

AuthorizationFilter.java
...
errors.add(ActionErrors.GLOBAL_MESSAGE,
         new ActionMessage("error.authentication.required"));
...


The key “error.authentication.required” is a reference to a message located in a properties file:

application.properties
################# ERRORS ###########################
#STYLE
errors.header = <div style="color:red;"><ul>
errors.footer = </ul></div>
errors.prefix = <li>
errors.suffix = </li>

#SECURITY
error.authentication.required = You have no permission to access this resource.
error.authorization.nopermission = You ({0}) have no permission to access this resource.


As you can see there is a collection of keys (errors.header, errors.footer, errors.prefix, errors.suffix) which allows to change the way the errors will be displayed.

This application.properties is set in struts-config.xml including its path:

struts-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC
 "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN"
 "http://jakarta.apache.org/struts/dtds/struts-config_1_3.dtd">

<struts-config>
 <form-beans>
    <form-bean name="loginForm"
 type="com.hgro.beta.form.LoginForm"/>
 </form-beans>

 <action-mappings>
    <action path="/login"
   type="com.hgro.beta.action.LoginAction"
   name="loginForm"
   input="/login.jsp"
   scope="request"
   validate="true">
   <forward name="adminsuccess" path="/admin/menu.jsp"/>
   <forward name="usersuccess" path="/user/menu.jsp"/>
   <forward name="failure" path="/login.jsp"/>
     </action>
   
     <action path="/welcome" forward="/login.jsp" />
   
     <action path="/user/menu" forward="/user/menu.jsp" />
     <action path="/admin/menu" forward="/admin/menu.jsp" />
 </action-mappings>

 <message-resources parameter="com.jml.tutorial.application" />
</struts-config>


As you can see, struts-config.xml maps all the actions of users and administrators under the protected directories.

Finally, login.jsp will have to display the errors using the struts html tag “errors” as follows:

login.jsp
...
<%@ taglib uri="http://struts.apache.org/tags-html"  prefix="html" %>
...
<html:html>
 ...
 <body>
  ...
  <html:errors  />
  ...
 </body>
</html:html>


You can find an example of this tutorial here.

References:

No comments:

Post a Comment

Post a Comment