Monday, May 30, 2016

Basic Authentication/Validation for Webservices using REST API and Interceptors

Hi All,
Thanks for reading this post...

Let's talk about how to implement a set of classes using REST concepts, and basic control access to the WebServices. An interceptor will be responsible for reading the request and manage the access control to the resources.

First of all, it will be necessary to define the pom.xml (maven resources) with the following dependencies:

        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_1.1_spec</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-crypto</artifactId>
            <version>${resteasy.version}</version>
            <scope>provided</scope>
        </dependency>
       
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jaxb-provider</artifactId>
            <version>${resteasy.version}</version>
            <scope>provided</scope>
        </dependency>

JBoss EAP 6.4 and Resteasy API 2.3.10.Final were used to validate the sample code.

 - Rest Activator

@ApplicationPath("/rest")
public class WsRestActivator extends Application {
}

- Exception Class

public class ServiceAPIException extends Exception {

   public ServiceAPIException(String message) {
        super(message);
    }

    public ServiceAPIException(Exception e) {
        super(e);
    }

    public ServiceAPIException(String msg, Throwable t) {
        super(msg, t);
    }
}

@Provider
public class ExceptionHttpStatusResolver implements ExceptionMapper<ServiceAPIException> {

    @Override
    public Response toResponse(ServiceAPIException exception) {
        Response.Status httpStatus = Response.Status.INTERNAL_SERVER_ERROR;

        if (exception instanceof ServiceAPIException)
            httpStatus = Response.Status.BAD_REQUEST;

        return Response.status(httpStatus).entity(exception.getMessage()).build();
    }
}

- Annotation to allow access to the Webservice methods

@Documented
@Retention (RUNTIME)
@Target({TYPE, METHOD})
public @interface WsAccessMethodPermition {
}

- Service Abstracttion

public abstract class ServiceRest {

    protected void handleException(String msg, Throwable t) throws ServiceAPIException {
        throw new ServiceAPIException(msg, t);
    }
}

- Security Abstraction for Resource Classes

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.security.smime.EnvelopedOutput;


public abstract class SecureResource extends ServiceRest {

    public static final String USER_CERTIFICATE = "user-certificate";
   
    private static CertificateFactory factory;

    static {
        try {
            factory = CertificateFactory.getInstance("X.509");
        } catch (Exception e) {

        }
    }

    protected X509Certificate extractCertificate(HttpServletRequest request) {
        try {
            InputStream is = new ByteArrayInputStream((byte[]) request.getAttribute(
                    SecureResource.USER_CERTIFICATE));
            return (X509Certificate) factory.generateCertificate(is);
        } catch (CertificateException e) {
            return null;
        }
    }
   
    protected EnvelopedOutput encryptResponse(HttpServletRequest request, Object result) {
        X509Certificate certificate = extractCertificate(request);
        return encryptResponse(certificate, result);
    }
   
    protected EnvelopedOutput encryptResponse(X509Certificate certificate, Object result) {
        // Object user = UserContext.getUser(result);
        EnvelopedOutput output = new EnvelopedOutput(
                new Object()/* your user object */, MediaType.APPLICATION_JSON);
        output.setCertificate(certificate);

        return output;
    }

}

- Interceptor

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

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.inject.Inject;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.ext.Provider;

import org.jboss.resteasy.annotations.interception.ServerInterceptor;
import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.core.ResourceMethod;
import org.jboss.resteasy.core.ServerResponse;
import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.interception.AcceptedByMethod;
import org.jboss.resteasy.spi.interception.PreProcessInterceptor;
import org.jboss.resteasy.util.Base64;

@Provider
@ServerInterceptor
public class SecurityInterceptor implements PreProcessInterceptor, AcceptedByMethod {

    private static final String          AUTHORIZATION_PROPERTY = "Authorization";

    private static final String          AUTHENTICATION_SCHEME  = "Basic";

    private static final ServerResponse  ACCESS_DENIED          = new ServerResponse("Access Denied!",     401, new Headers<Object>());

    private static final ServerResponse  ACCESS_FORBIDDEN       = new ServerResponse("Access Forbidden!",  403, new Headers<Object>());

    private static final ServerResponse  SERVER_ERROR           = new ServerResponse("Internal Error!",    500, new Headers<Object>());

    @Override
    public ServerResponse preProcess(HttpRequest request, ResourceMethod methodInvoked) throws Failure, WebApplicationException {
        Method method = methodInvoked.getMethod();

        if (method.isAnnotationPresent(WsAccessMethodPermition.class) ||
                method.isAnnotationPresent(PermitAll.class)) {
            return null;
        }

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

        // Get request headers
        final HttpHeaders headers = request.getHttpHeaders();

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

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

        // Get encoded username and password
        final String encodedUserPassword = authorization.get(0).replaceFirst(
                AUTHENTICATION_SCHEME.concat(" "), "");

        // Decode username and password
        String usernameAndPassword;
        try {
            usernameAndPassword = new String(Base64.decode(encodedUserPassword));
        } catch (IOException e) {
            return SERVER_ERROR;
        }

        // 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 (!isUserAllowed(request, username, password)) {
            return ACCESS_DENIED;
        }

        return null;
    }

    @SuppressWarnings("serial")
    private boolean isUserAllowed(final HttpRequest request, final String username, final String password) {
        UserWS user = new UserWS(username, password);
        if (user != null) {
            if (!user.getRoles().isEmpty() && !user.hasPermission()) {

                byte[] certificate = user.getCertificate();
                request.setAttribute(SecureResource.USER_CERTIFICATE, certificate);
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean accept(@SuppressWarnings("rawtypes") Class declaring, Method method) {

        // bypass authentication - get methods for instance
        if (method.isAnnotationPresent(WsAccessMethodPermition.class) ||
                method.isAnnotationPresent(PermitAll.class)) {
            return false;
        }

        // Access denied for all
        if (method.isAnnotationPresent(DenyAll.class)) {
            // TODO here for instance log denied access
            return true;
        }

        return true;
    }
}

And now...the resource...

- WebService Implementation

@Path("/test")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class TestResourceWS extends SecureResource {

    @POST
    @Path("/info/")
    @Produces(MediaType.APPLICATION_JSON)
    @WsAccessMethodPermition
    @Override
    public String info() throws ServiceAPIException {
        return "Hello, Access OK!";
    }

    @POST
    @Path("/infodenied/")
    @Produces(MediaType.APPLICATION_JSON)
    @Override
    public void infodenied() throws ServiceAPIException {
        // "denied!";
    }
}

- Sample invocation

http://<host>/<app-name>/rest/test/info
OK!

http://<host>/<app-name>/rest/test/infodenied
Access Denied!

- WS Client Sample...post methods for instance

   MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
   headers.add("Authorization", "Basic " + base64Creds);
   headers.add("Content-Type", "application/json");
   RestTemplate restTemplate = new RestTemplate();
   HttpEntity<ObjectToPass> request = new HttpEntity<ObjectToPass>(ObjectToPass, headers);
     restTemplate.postForObject("http://<host>/<app>/rest/test/info", request, Boolean.class);


Thanks a lot and regards!

No comments:

Post a Comment