Jersey (JAX-RS) SecurityContext in action

Someone ask me how to use SecurityContext and RolesAllowed with the previous article. Here is.

Creating User

Before anything else, we need to create a role based user (this bean is the one we will you use to logon, check user, …):

package com.myapp.bean;

import java.security.Principal;
import java.util.List;

/**
 * User bean.
 *
 * @author Deisss (MIT License)
*/
public class User implements Principal {
    private String id, firstName, lastName, login, email, password;
    private List<String> role;

    public void setId(String id) {this.id = id;}
    public String getId() {return this.id;}
    public String getFirstName() {return this.firstName;}
    public void setFirstName(String firstName) {this.firstName = firstName;}
    public String getLastName() {return this.lastName;}
    public void setLastName(String lastName) {this.lastName = lastName;}
    public String getLogin() {return login;}
    public void setLogin(String login) {this.login = login;}
    public String getEmail() {return email;}
    public void setEmail(String email) {this.email = email;}
    public String getPassword() {return password;}
    public void setPassword(String password) {this.password = password;}
    public List<String> getRole() {return role;}
    public void setRole(List<String> role) {this.role = role;}

    @Override
    public String getName() {
        return this.firstName + " " + this.lastName;
    }
}

The getName comes from Principal interface, we will not use it so much here. Now we got your database user, we need to create a security context for it.

Security context

The security context allow you to define some basic security, mostly surround the use of role based access, and HTTP/HTTPS access. It is the root of allow/deny mechanism in Jersey:

package com.myapp.security;

import com.myapp.bean.User;
import javax.ws.rs.core.SecurityContext;
import java.security.Principal;

/**
 * Custom Security Context.
 * 
 * @author Deisss (MIT License)
*/
public class MyApplicationSecurityContext implements SecurityContext {
    private User user;
    private String scheme;

    public CustomSecurityContext(User user, String scheme) {
        this.user = user;
        this.scheme = scheme;
    }

    @Override
    public Principal getUserPrincipal() {return this.user;}

    @Override
    public boolean isUserInRole(String s) {
        if (user.getRole() != null) {
            return user.getRole().contains(s);
        }
        return false;
    }

    @Override
    public boolean isSecure() {return "https".equals(this.scheme);}

    @Override
    public String getAuthenticationScheme() {
        return SecurityContext.BASIC_AUTH;
    }
}

A little explain just in case:

  • getUserPrincipal got no use here (as far as I know), but this is reason why we have implemented Principal in the user bean above.
  • isUserInRole is the main function we will use here. As the user bean uses internally a list, we need to check a given roles exists in the user role list.
  • isSecure simply indicates if we are in HTTPS or not. Not used here but you can create a filter with this to deny non-HTTPS request for example.
  • getAuthenticationScheme the scheme used, here it will be HTTP Basic Auth.

As you see, you got everything to handle every possible interaction with any user here. Now we still need to lock down resources, and create a filter to populate our SecurityContext.

Filtering

I will here take the previous filter, from the previous article to add the SecurityContext check at the same time.

/**
 * Allow to encode/decode the authentification.
 *
 * @author Deisss (LGPLv3)
 */
public class BasicAuth {
    /**
     * Decode the basic auth and convert it to array login/password
     * @param auth The string encoded authentification
     * @return The login (case 0), the password (case 1)
     */
    public static String[] decode(String auth) {
        //Replacing "Basic THE_BASE_64" to "THE_BASE_64" directly
        auth = auth.replaceFirst("[B|b]asic ", "");
 
        //Decode the Base64 into byte[]
        byte[] decodedBytes = DatatypeConverter.parseBase64Binary(auth);
 
        //If the decode fails in any case
        if(decodedBytes == null || decodedBytes.length == 0){
            return null;
        }
 
        //Now we can convert the byte[] into a splitted array :
        //  - the first one is login,
        //  - the second one password
        return new String(decodedBytes).split(":", 2);
    }
}

This is our decoder to decode a Basic Auth encoded string to an array of login/password couple. Now the filter:

package com.myapp.filter;

import com.myapp.security.MyApplicationSecurityContext;
import com.myapp.bean.User;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.container.PreMatching;
 
/**
 * Jersey HTTP Basic Auth filter
 * @author Deisss (LGPLv3)
 */
@Provider
@PreMatching
public class AuthFilter implements ContainerRequestFilter {
    /**
     * Apply the filter : check input request, validate or not with user auth
     * @param containerRequest The request from Tomcat server
     */
    @Override
    public void filter(ContainerRequestContext containerRequest) throws WebApplicationException {
        //GET, POST, PUT, DELETE, ...
        String method = containerRequest.getMethod();
        // myresource/get/56bCA for example
        String path = containerRequest.getUriInfo().getPath(true);
 
        //We do allow wadl to be retrieve
        if(method.equals("GET") && (path.equals("application.wadl") || path.equals("application.wadl/xsd0.xsd")){
            return;
        }
 
        //Get the authentification passed in HTTP headers parameters
        String auth = containerRequest.getHeaderString("authorization");
 
        //If the user does not have the right (does not provide any HTTP Basic Auth)
        if(auth == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
 
        //lap : loginAndPassword
        String[] lap = BasicAuth.decode(auth);
 
        //If login or password fail
        if(lap == null || lap.length != 2) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
 
        //DO YOUR DATABASE CHECK HERE (replace that line behind)...
        User authentificationResult =  AuthentificationThirdParty.authentification(lap[0], lap[1]);
 
        //Our system refuse login and password
        if(authentificationResult == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
 
        // We configure your Security Context here
        String scheme = request.getUriInfo().getRequestUri().getScheme();
        request.setSecurityContext(new MyApplicationSecurityContext(user, scheme);

        //TODO : HERE YOU SHOULD ADD PARAMETER TO REQUEST, TO REMEMBER USER ON YOUR REST SERVICE...
    }
}

Ok, so we add only two lines to add our SecurityContext to it, and it’s not enough (of course), we still need to say to Jersey « we want to use SecurityContext », and also, we need to declare our filter.

Declaring Application

I didn’t find a better way, but if someone has something about registering SecurityContext usage directly in web.xml instead of this…

package com.myapp.app;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;

/**
 * Simple application startup.
 *
 * @author Deisss (MIT License)
*/
public class MyApplication extends ResourceConfig {
    public MyApplication() {
        register(RolesAllowedDynamicFeature.class);
    }
}

Silly class for just this… Now we need to register it to web.xml, but we will do that at the end…

Creating a resource

Of course, all of this will have no interest if there not an example to run it:

package com.myapp.api;

import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

/**
 * Simple resource which rely on two roles: "user" and "admin".
 *
 * @author Deisss (MIT License)
*/
@Path("security")
@PermitAll
public class SecurityResource {
    @GET
    @Path("user")
    @RolesAllowed("user")
    public String user(@Context SecurityContext sc) {
        boolean test = sc.isUserInRole("user");
        return (test) ? "true": "false";
    }

    @GET
    @Path("admin")
    @RolesAllowed("admin")
    public String admin(@Context SecurityContext sc) {
        boolean test = sc.isUserInRole("admin");
        return (test) ? "true": "false";
    }
}

That’s of course an example. If the user got the role « user », it can access /security/user path, which will reply « true » or « false » if the user has or not the role « user ». Of course, as we are creating a security context, when this will be activated, it will only print « true » or « HTTP 403 deny access ».
The same goes for « admin » role.
The example here show two way to use SecurityContext. The best one is of course, using the RolesAllowed annotation, the second one can be handy sometimes if you need something quite particular, by injecting the context as a parameter.

If you run this, it will not work. Simple: we didn’t configure web.xml yet. Let’s do this.

The final: web.xml

We got quite few things to register in our web.xml here: filters, resourceConfig, … After this setup, the application and the associated security context should works:

<?xml version="1.0" encoding="UTF-8"?>
<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">
    <display-name>MyApp</display-name>
    <servlet>
        <servlet-name>MyApp</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <!-- The package now NEED to reference all classes used on startup -->
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>
                com.myapp.api;
                com.myapp.app;
                com.myapp.filter;
                org.glassfish.jersey.filter;
            </param-value>
        </init-param>

        <!-- Application -->
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.myapp.app.MyApplication</param-value>
        </init-param>
        
        <!-- Filters -->
        <init-param>
            <param-name>javax.ws.rs.container.ContainerRequestFilter</param-name>
            <param-value>
                com.myapp.filter.AuthFilter;
                org.glassfish.jersey.filter.LoggingFilter;
            </param-value>
        </init-param>
        <init-param>
            <param-name>javax.ws.rs.container.ContainerResponseFilters</param-name>
            <param-value>
                org.glassfish.jersey.filter.LoggingFilter;
            </param-value>
        </init-param>

        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>MyApp</servlet-name>
        <url-pattern>/myapp/*</url-pattern>
    </servlet-mapping>
</web-app>

That’s it, the important part are:

  • javax.ws.rs.Application: will start MyApplication class, and register RolesAllowedDynamicFeature as a used resource to Jersey (will activate SecurityContext use)
  • javax.ws.rs.container.ContainerRequestFilter: will register our filter as a used one on every request
  • jersey.config.server.provider.packages: where you will put all packages jersey need to access in this web.xml

Now everything should be OK. I strongly recommand to check A LOT security context to be working, before anything else, I’ve found during test that you have almost no indication to know if security context is in use or not. And if, for any reason, it’s not in use, it means the RolesAllowed annotation will not work (allowing everybody to access anything, no matter what their role are…).
I would recommand for that, to keep a basic resource like the one above, to always call it during your unit test check, to be sure that security context is well setup and working…

Publicités

3 Commentaires

  1. Thank you so much! This is incredibly helpful!

  2. in class AuthFilter the object « request » (not containerRequest) from where arrives ???

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :