Wildfly web.xml security constraint blocking basic auth header for JAX-RS methods using ContainerRequestFilter

PawelPredki picture PawelPredki · Aug 17, 2014 · Viewed 9.1k times · Source

The web application I'm developing consists of some servlets and also JAX-RS webservices. Until now, I was using a ContainerRequestFilter to authenticate the REST method calls but now I also need to secure the servlets so I decided to use web.xml to define security constraints. My web.xml looks like this:

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>rest</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
    </security-constraint>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/protected/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>
    <security-role>
        <role-name>admin</role-name>
    </security-role>
    <security-role>
        <role-name>user</role-name>
    </security-role>
    <!-- Configure login to be HTTP Basic -->
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>Restricted Zone</realm-name>
    </login-config>
</web-app>

If I understand the syntax of web.xml correctly, what I defined means that the access to /rest/* (where all my JAX-RS methods are) is unrestricted as far as the LoginModules are concerned, and all access to the /protected/* path (where I keep my secure servlets) requires Basic Authorization.

When I try to open one of the secure servlets, e.g. /protected/test, I get the basic auth login dialog in the browser and the behavior is correct - if I enter credentials for an 'admin' user, I'm allowed access. Otherwise, I get a 'Forbidden' message.

Also, when I try to access anything on the /rest/ path I get no basic auth dialog, which is what I would expect. However, the Authorization header I get in the ContainerRequestFilter is not the one I'm sending in the REST request but it's the one I used previously to get into the /protected/ servlet.

Below are other parts of the puzzle:

standalone.xml (security-domains section)

<security-domain name="PaloSecurityDomain" cache-type="default">
    <authentication>
        <login-module code="com.palo.security.PaloLoginModule" flag="required"/>
    </authentication>
</security-domain>

jboss-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
    <security-domain>PaloSecurityDomain</security-domain>
</jboss-web>

PaloLoginModule.java

package com.palo.security;

import java.security.acl.Group;
import java.util.Set;

import javax.inject.Inject;
import javax.naming.NamingException;
import javax.security.auth.login.LoginException;

import org.apache.log4j.Logger;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.auth.spi.UsernamePasswordLoginModule;

import com.palo.PaloRealmRole;
import com.palo.model.PaloRealmUser;
import com.palo.utils.CdiHelper;
import com.palo.utils.PasswordHandler;

public class PaloRealmLoginModule extends UsernamePasswordLoginModule {

  private static Logger logger = Logger
      .getLogger(PaloRealmLoginModule.class);

  @Inject
  private PaloRealmLogic realmLogic;

  @Override
  protected String getUsersPassword() throws LoginException {
    if (null == realmLogic) {
      try {
        CdiHelper.programmaticInjection(PaloRealmLoginModule.class,
            this);
      } catch (NamingException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
    logger.debug("Getting password for user " + super.getUsername());
    PaloRealmUser user = realmLogic.getUserByName(super.getUsername());
    if (null == user) {
      logger.error("User not found");
      throw new LoginException("User " + super.getUsername()
          + " not found");
    }
    logger.debug("Found " + user.getPassword());
    return user.getPassword();
  }

  @Override
  protected Group[] getRoleSets() throws LoginException {
    logger.debug("Getting roles for user " + super.getUsername());
    if (null == realmLogic) {
      try {
        CdiHelper.programmaticInjection(PaloRealmLoginModule.class,
            this);
      } catch (NamingException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
    PaloRealmUser user = realmLogic.getUserByName(super.getUsername());
    if (null == user) {
      throw new LoginException("User " + super.getUsername()
          + " not found");
    }
    Set<PaloRealmRole> roles = user.getRoles();
    Group[] groups = { new SimpleGroup("Roles") };
    for (PaloRealmRole role : roles) {
      logger.debug("Found role " + role.getRole());
      SimplePrincipal prole = new SimplePrincipal(role.getRole());
      groups[0].addMember(prole);
    }

    return groups;
  }

  @Override
  protected boolean validatePassword(String inputPassword,
      String expectedPassword) {
    logger.debug("Validating password " + inputPassword + "|"
        + expectedPassword);
    return PasswordHandler.getInstance().verifyPassword(inputPassword,
        expectedPassword);
  }

}

SecurityInterceptor.java

package com.palo.web.rest;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.StringTokenizer;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.inject.Inject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;

import org.apache.log4j.Logger;
import org.jboss.resteasy.annotations.interception.ServerInterceptor;
import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.core.ServerResponse;

import com.palo.analytics.GoogleAnalyticsEvent;
import com.palo.logic.UserLogic;
import com.palo.web.utils.HttpUtils;

@Provider
@ServerInterceptor
public class SecurityInterceptor implements ContainerRequestFilter {

  private static Logger logger = Logger.getLogger(SecurityInterceptor.class);

  private static final String AUTHORIZATION_PROPERTY = "Authorization";
  private static final ServerResponse ACCESS_DENIED = new ServerResponse(
      "Access denied for this resource", 401, new Headers<Object>());
  private static final ServerResponse ACCESS_DENIED_FOR_USER = new ServerResponse(
      "User not authorized", 401, new Headers<Object>());
  private static final ServerResponse ACCESS_FORBIDDEN = new ServerResponse(
      "Nobody can access this resource", 403, new Headers<Object>());

  @Inject
  private UserLogic ul;

  @Override
  /**
   * The request filter is called automatically called for each incoming request. It checks which method is being called by the client and, based on that method's annotations, restricts access, verifies the identity of the caller, checks the validity of the session token, etc.
   */
  public void filter(ContainerRequestContext requestContext)
      throws IOException {
    logger.debug("------------- request filter ------------");
    ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) requestContext
        .getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
    Method method = methodInvoker.getMethod();
    String methodName = method.getName();
    String uri = requestContext.getUriInfo().getPath();

    logger.debug("Accessing method " + methodName + " via URI " + uri);

    for (String str : requestContext.getPropertyNames()) {
      logger.debug(str);
    }

    // Get request headers
    final MultivaluedMap<String, String> headers = requestContext
        .getHeaders();
    for (String key : headers.keySet()) {
      for (String value : headers.get(key)) {
        logger.debug(key + " - " + value);
      }
    }

    // Access allowed for all
    if (method.isAnnotationPresent(PermitAll.class)) {
      return;
    }
    // Access denied for all
    if (method.isAnnotationPresent(DenyAll.class)) {
      requestContext.abortWith(ACCESS_FORBIDDEN);
      return;
    }

    // Fetch authorization header
    final List<String> authorization = headers.get(AUTHORIZATION_PROPERTY);

    // If no authorization information present; block access
    if (null == authorization || authorization.isEmpty()) {
      requestContext.abortWith(ACCESS_DENIED);
      return;
    }

    final String username = HttpUtils.getUsernameFromAuthorizationHeader(
        authorization, HttpUtils.AUTHENTICATION_SCHEME_BASIC);
    final String password = HttpUtils.getPasswordFromAuthenticationHeader(
        authorization, HttpUtils.AUTHENTICATION_SCHEME_BASIC);

    if (null == username || null == password || username.isEmpty()
        || password.isEmpty()) {
      requestContext.abortWith(ACCESS_DENIED_FOR_USER);
      return;
    }

    boolean authenticated = ul.authenticate(username, password);
    if (false == authenticated) {
      requestContext.abortWith(ACCESS_DENIED);
      return;
    } 
    return;
  }
}

I'm using RESTClient for Firefox to send the REST requests to the JAX-RS methods. Since I'm logging all the headers, I can plainly see what comes to the Filter and the value doesn't change between calls, even if I change it in RESTClient. What is more, the value is still there even if I don't use the Authorization header in RESTClient.

My question is why is the Authorization header blocked and it's not forwarded to my Filter? If I remove the web.xml file, I get the correct Authorization header in the ContainerRequestFilter. Is there any way to move the /rest part of the application to a zone that is not affected by the login-config in web.xml?

Any help is greatly appreciated!

Answer

Zhenya picture Zhenya · Dec 19, 2014

From what I understand, if you specify the login-config, it's then used for all resources, specified in web-resource-collection. Both /rest/ and /protected/ in your case.

First approach One thing you could do, is modify your login module, so that it assigns admin role to those users who have provided valid credentials, and assigns anonymous role to those, who has not provided valid credentials. Then you could modify your web.xml like this

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>rest</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>anonymous</role-name>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>protected</web-resource-name>
            <url-pattern>/protected/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>


Second approach instead of modifying your login module, is adding one more login module to your security domain, which would assign anonymous role to everyone

Third approach Use Custom Authentication Mechanism http://undertow.io/documentation/core/security.html The BASIC authentication mechanism expects the user to send the credentials in http header in format Authorization: Basic: base64encodedCredentials

When using custom authentication mechanism, you have access to the request path, and you could make your custom authentication mechanism skip the call to the login modules in case the request is made to the path which you don't want to be protected. But I don't think this is a good approach, as these kinds of decisions should be made by login modules+web.xml.


Fourth approach (not sure if it works, but hopefully it does Resources, which are not specified in security-constraints, aren't checked by login modules. So, to make /rest/ resource unprotected, remove these lines from your web.xml:

<security-constraint>
        <web-resource-collection>
            <web-resource-name>rest</web-resource-name>
            <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
    </security-constraint>