How do I configure both BASIC and FORM authentication methods in the same Java EE application

5.4k Views Asked by At

I need to configure both BASIC and FORM authentication methods depending on web resource in my Java EE application. It means that for example for path /app/services I want to authenticate using BASIC and for the rest of the application method would be FORM.

Is it even possible without Spring but in pure Java EE?

4

There are 4 best solutions below

0
On

It's possible, but you'd need to create and install your own authentication module instead of using the two build-in BASIC and FORM ones.

Java EE has an API/SPI for this called JASPIC. On top of that, many application servers have an alternative native API for this.

0
On

Spring security installs its own filter. You can write your own filter/filters and install them instead. If you can easily identify which URL patterns use one auth and which the other it should be fairly simple with two filters

0
On

Starting with Servlet 3.0 (Tomcat 7+) you can programmatically do HttpServletRequest.login()

Get user:pass as in romanov's answer and login.

0
On

Yes, there is a workaround (I did it for Tomcat 7.0.68).

1) Configure your web.xml to use FORM auth-method:

<login-config>
 <auth-method>FORM</auth-method>
  <form-login-config>
    <form-login-page>/login.jsp</form-login-page>
    <form-error-page>/loginError.jsp</form-error-page>
  </form-login-config>
</login-config>

2) Set the url-pattern you want to authenticate BASIC way with no auth-constraint:

<security-constraint>
  <web-resource-collection>
    <web-resource-name>BASIC auth path</web-resource-name>
      <url-pattern>/app/services/*</url-pattern>
    </web-resource-collection>
  </security-constraint>

3) Configure a filter for that pattern:

<filter>
  <filter-name>BasicLoginFilter</filter-name>
  <filter-class>pa.cka.ge.BasicLoginFilter</filter-class>
  <init-param>
    <param-name>role-names-comma-sep</param-name>
    <param-value>role1,andRole2,andRole3</param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>BasicLoginFilter</filter-name>
  <url-pattern>/app/services/*</url-pattern>
</filter-mapping>

enter code here

where role-names-comma-sep is your custom parameter that defines roles to access /app/services. This is useful because the path /app/services must have no auth-constraints (see above) and you basically have no way to define roles as usual. In my example, the implementation checks those roles by AND (you can change it).

4) Process login manually in the filter:

package pa.cka.ge;

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

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 org.apache.catalina.Role;
import org.apache.catalina.users.MemoryUser;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.codec.binary.Base64;

public class BasicLoginFilter implements Filter {

  /**
   * List of roles the user must have to authenticate
   */
  private final List<String> roleNames = new ArrayList<String>();

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    String roleNamesParam = filterConfig.getInitParameter("role-names-comma-sep");
    if (roleNamesParam != null) {
      for (String roleName: roleNamesParam.split(",")) {
        roleNames.add(roleName);
      }
    }
  }

  private static final String AUTHORIZATION_HEADER = "Authorization";
  private static final String BASIC_PREFIX = "Basic ";

  @Override
  public void doFilter(ServletRequest req, ServletResponse resp,
      FilterChain chain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)resp;

    // get username and password from the Authorization header
    String authHeader = request.getHeader(AUTHORIZATION_HEADER);
    if (authHeader == null || !authHeader.startsWith(BASIC_PREFIX)) {
      throwBasicAuthRequired();
    }

    String userPassBase64 = authHeader.substring(BASIC_PREFIX.length());
    String userPassDecoded = new String(Base64.decodeBase64(userPassBase64), B2CConverter.ISO_8859_1);// decode from base64 any other way, if this won't work for you. Finally userPassDecoded must contain readable "username:password"
    if (!userPassDecoded.contains(":")) {
      throwBasicAuthRequired();
    }

    String authUser = userPassDecoded.substring(0, userPassDecoded.indexOf(':'));
    String authPass = userPassDecoded.substring(userPassDecoded.indexOf(':') + 1);

    // do login manually
    request.login(authUser, authPass);

    // check roles for the user
    final Principal userPrincipal = request.getUserPrincipal();

    // Your Principal will be another class, not MemoryUser. Run in debug mode to see what class you actually have. The role checking will depend on that class.
    MemoryUser user = (MemoryUser)userPrincipal; 

    boolean hasRoles = true;
    for (String role: roleNames) {
      if (role == null) {
        continue;
      }
      boolean hasRole = false;
      Iterator<Role> roles = user.getRoles();
      while (roles.hasNext()) {
        if (role.equals(roles.next().getName())) {
          hasRole = true;
          break;
        }
      }
      if (!hasRole) {
        hasRoles = false;
        break;
      }
    }

    if (hasRoles) {
      // login successful
      chain.doFilter(request, response);
      request.logout();// optional
    } else {
      // login failed
      throwLoginFailed();
    }
  }

  @Override
  public void destroy() {
  }

  public static void throwBasicAuthRequired() throws ServletException {
    throw new ServletException("The /app/services resources require BASIC authentication");
  }

  public static void throwLoginFailed() throws ServletException {
    throw new ServletException("Login failed");
  }
}

Done! Now /app/services support BASIC auth, but the rest application supports FORM.