Tuesday, July 5, 2016

Java : Step-By-Step process to secure your RESTful API using ResourceConfig and @RolesAllowed annotations.

It is a very good practice to secure your web services so that the user of that service can be given a limited access to the methods based on the roles they have. We are going to user @RolesAllowed annotations in this section along with ResoourceConfig class. We will go through step by step procedure for this:

1) Modify your web.xml with the below mentioned code:

<?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" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <servlet-name>jersey-serlvet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
         <init-param>
       <param-name>jersey.config.server.provider.packages</param-name>
       <param-value>com.vagalla.jersey.first</param-value>
    </init-param>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.vagalla.jersey.first.MyApplication</param-value>
        </init-param>      
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>jersey-serlvet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>


In this section we have mentioned that every request which will hit the /rest uri, will undergo the ServletContainer and MyAppication will be loaded as the initial parameters.

2) Create class MyApplication extending the ResourceConfig internal class like follows:


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

public class MyApplication extends ResourceConfig {
    public MyApplication() {
        super(Hello.class);
        //register(RolesAllowedDynamicFeature.class);
        register(AuthenticationFilter.class);
    }
}

Here, in this class we are registering a class named AuthenticationFilter.class, LoggingFilter is a deprecated one. We can also register RolesAllowedDynamicFeature.class if we want. But, here in our example, only AuthenticationFilter.class is needed for simple HTTP basic authentication.

3) Create public class AuthenticationFilter implements javax.ws.rs.container.ContainerRequestFilter.

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;

import org.glassfish.jersey.internal.util.Base64;

/**
 * This filter verify the access permissions for a user
 * based on username and passowrd provided in request
 * */
@Provider
@PreMatching
public class AuthenticationFilter implements javax.ws.rs.container.ContainerRequestFilter
{
    
    @Context
    private ResourceInfo resourceInfo;
     
    private static final String AUTHORIZATION_PROPERTY = "Authorization";
    private static final String AUTHENTICATION_SCHEME = "Basic";
    private static final Response ACCESS_DENIED =       Response.status(Response.Status.UNAUTHORIZED)
                                                        .entity("You cannot access this resource").build();
    private static final Response ACCESS_FORBIDDEN = Response.status(Response.Status.FORBIDDEN)
                                                        .entity("Access blocked for all users !!").build();
    
      
    @Override
    public void filter(ContainerRequestContext requestContext)
    {
        Method method = resourceInfo.getResourceMethod();
        //Access allowed for all
        if( ! method.isAnnotationPresent(PermitAll.class))
        {
            //Access denied for all
            if(method.isAnnotationPresent(DenyAll.class))
            {
                requestContext.abortWith(ACCESS_FORBIDDEN);
                return;
            }
              
            //Get request headers
            final MultivaluedMap<String, String> headers = requestContext.getHeaders();
              
            //Fetch authorization header
            final List<String> authorization = headers.get(AUTHORIZATION_PROPERTY);
            //If no authorization information present; block access
            if(authorization == null || authorization.isEmpty())
            {
                requestContext.abortWith(ACCESS_DENIED);
                return;
            }
              
            //Get encoded username and password
            final String encodedUserPassword = authorization.get(0).replaceFirst(AUTHENTICATION_SCHEME + " ", "");
              
            //Decode username and password
            String usernameAndPassword = new String(Base64.decode(encodedUserPassword.getBytes()));;
  
            //Split username and password tokens
            final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":");
            final String username = tokenizer.nextToken();
            final String password = tokenizer.nextToken();
              
            //Verify user access
            if(method.isAnnotationPresent(RolesAllowed.class))
            {
                RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
                Set<String> rolesSet = new HashSet<String>(Arrays.asList(rolesAnnotation.value()));
                  
                //Is user valid?
                if( ! isUserAllowed(username, password, rolesSet))
                {
                    requestContext.abortWith(ACCESS_DENIED);
                    return;
                }
            }
        }
    }
    private boolean isUserAllowed(final String username, final String password, final Set<String> rolesSet)
    {
        boolean isAllowed = false;

        if(username.equals("julie") && password.equals("qwerty"))
        {
            String userRole = "member";
            //Step 2. Verify user role
            if(rolesSet.contains(userRole))
            {
                isAllowed = true;
            }
        }
        return isAllowed;
    }
}

Here, in this class we are simply fetching the header from the request and fetching the authentication instance from that to find out the username , password, roles etc inforamtion. In the method isUserAllowed, there can be code for the users authentication and we can go to the database to verify the username and password authenticity. isAnnotationPresent method can be used to find out the which annotation is applied over the said method.

4) Finally, create our service class where we have defined the resources with the specific URI's.

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

// Plain old Java Object it does not extend as class or implements 
// an interface

// The class registers its methods for the HTTP GET request using the @GET annotation. 
// Using the @Produces annotation, it defines that it can deliver several MIME types,
// text, XML and HTML. 

// The browser requests per default the HTML MIME type.

//Sets the path to base URL + /test
@Path("test")
@PermitAll
public class Hello {

    @GET
    @Path("editor")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("editor")
    public String editorOnly() {
        return "Got to editor path!";
    }

    @GET
    @Path("member")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("member")
    public String memberOnly() {
        return "Got to member path!";
    }

    @GET 
    @Path("open")
    @Produces(MediaType.TEXT_PLAIN)
    public String open(@Context SecurityContext context) {
        return "Open to all! - " + context.getUserPrincipal().getName();
    }
}

Run the program on server and try to access the method with @RolesAllowed("member") and it won't go through the code if the username and password doesn't match with julie and qwert with role as member.

No comments:

Post a Comment