Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access Jersey resource secured by @RolesAllowed

We were testing a REST webservice developed in jersey through postman rest client. It is a POST method and is annotated with @RolesAllowed. The full annotation the method is as follows:

@POST
@Path("/configuration")
@RolesAllowed("admin")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)

When I requested this http://baseurl/configuration with the expected HTTP body content, I got 403 response(it is expected since it is allowed only for admin as it seems).

My doubt is how to access this service with the specified role via rest client.

like image 231
Tom Sebastian Avatar asked Sep 28 '15 06:09

Tom Sebastian


1 Answers

So it seems like you set up the RolesAllowedDynamicFeature, but you have no authentication happening to set up the user and roles. What the RolesAllowedDynamicFeature does is lookup the SecurityContext, and calls the SecurityContext.isUserInRole(<"admin">) to see if the user in the SecurityContext has the role.

I imagine you don't know how the SecurityContext is set. There are a couple of ways. The first is through the servlet authentication mechanism. You can see more at Securing Web Applications from the Java EE tutorial.

Basically you need to set up a security realm or security domain on the server. Every server has it's own specific way of setting it up. You can see an example here or how it would be done with Tomcat.

Basically the realm/domain contains the users allowed to access the web app. Those users have associated roles. When the servlet container does the authentication, whether it be Basic authentication or Form authentication, it looks up the user from the credentials, and if the user is authenticated, the user and its roles are associated with the request. Jersey gathers this information and puts it into the SecurityContext for the request.

If this seems a bit complicated, an easier way to just forget the servlet container authentication and just create a Jersey filter, where you set the SecurityContext yourself. You can see an example here. You can use whatever authentication scheme you want. The important part is setting the SecurityContext with the user information, wherever you get it from, maybe a service that accesses a data store.

See Also:

  • securing rest services in Jersey

UPDATE

Here is a complete example of the second option using the filter. The test is run by Jersey Test Framework. You can run the test as is

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.DatatypeConverter;

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.JerseyTest;

import static junit.framework.Assert.*;
import org.junit.Test;

public class BasicAuthenticationTest extends JerseyTest {

    @Provider
    @Priority(Priorities.AUTHENTICATION)
    public static class BasicAuthFilter implements ContainerRequestFilter {
        
        private static final Logger LOGGER = Logger.getLogger(BasicAuthFilter.class.getName());

        @Inject
        private UserStore userStore;

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            String authentication = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
            if (authentication == null) {
                throw new AuthenticationException("Authentication credentials are required");
            }

            if (!authentication.startsWith("Basic ")) {
                return;
            }
            
            authentication = authentication.substring("Basic ".length());
            String[] values = new String(DatatypeConverter.parseBase64Binary(authentication), 
                                         Charset.forName("ASCII")).split(":");
            if (values.length < 2) {
                throw new WebApplicationException(400);
            }
            
            String username = values[0];
            String password = values[1];
            
            LOGGER.log(Level.INFO, "{0} - {1}", new Object[]{username, password});
            
            User user = userStore.getUser(username);
            if (user == null) {
                throw new AuthenticationException("Authentication credentials are required"); 
            }
            
            if (!user.password.equals(password)) {
                throw new AuthenticationException("Authentication credentials are required");
            }
            
            requestContext.setSecurityContext(new MySecurityContext(user));
        }
    }
    
    static class MySecurityContext implements SecurityContext {
        
        private final User user;
        
        public MySecurityContext(User user) {
            this.user = user;
        }

        @Override
        public Principal getUserPrincipal() {
            return new Principal() {
                @Override
                public String getName() {
                    return user.username;
                }
            };
        }

        @Override
        public boolean isUserInRole(String role) {
            return role.equals(user.role);
        }

        @Override
        public boolean isSecure() { return true; }

        @Override
        public String getAuthenticationScheme() {
            return "Basic";
        }
        
    }

    static class AuthenticationException extends WebApplicationException {

        public AuthenticationException(String message) {
            super(Response
                    .status(Status.UNAUTHORIZED)
                    .header("WWW-Authenticate", "Basic realm=\"" + "Dummy Realm" + "\"")
                    .type("text/plain")
                    .entity(message)
                    .build());
        }
    }

    class User {

        public final String username;
        public final String role;
        public final String password;

        public User(String username, String password, String role) {
            this.username = username;
            this.password = password;
            this.role = role;
        }
    }

    class UserStore {

        public final Map<String, User> users = new ConcurrentHashMap<>();

        public UserStore() {
            users.put("peeskillet", new User("peeskillet", "secret", "USER"));
            users.put("stackoverflow", new User("stackoverflow", "superSecret", "ADMIN"));
        }

        public User getUser(String username) {
            return users.get(username);
        }
    }
    
    private static final String USER_RESPONSE = "Secured User Stuff";
    private static final String ADMIN_RESPONSE = "Secured Admin Stuff";
    private static final String USER_ADMIN_STUFF = "Secured User Admin Stuff";
    
    @Path("secured")
    public static class SecuredResource {
        
        @GET
        @Path("userSecured")
        @RolesAllowed("USER")
        public String getUser() {
            return USER_RESPONSE;
        }
        
        @GET
        @Path("adminSecured")
        @RolesAllowed("ADMIN")
        public String getAdmin() {
            return ADMIN_RESPONSE;
        }
        
        @GET
        @Path("userAdminSecured")
        @RolesAllowed({"USER", "ADMIN"})
        public String getUserAdmin() {
            return USER_ADMIN_STUFF;
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(SecuredResource.class)
                .register(BasicAuthFilter.class)
                .register(RolesAllowedDynamicFeature.class)
                .register(new AbstractBinder(){
            @Override
            protected void configure() {
                bind(new UserStore()).to(UserStore.class);
            }
        });
    }
    
    static String getBasicAuthHeader(String username, String password) {
        return "Basic " + Base64.encodeAsString(username + ":" + password);
    }
    
    @Test
    public void should_return_403_with_unauthorized_user() {
        Response response = target("secured/userSecured")
                .request()
                .header(HttpHeaders.AUTHORIZATION, 
                        getBasicAuthHeader("stackoverflow", "superSecret"))
                .get();
        assertEquals(403, response.getStatus());
    }
    
    @Test
    public void should_return_200_response_with_authorized_user() {
        Response response = target("secured/userSecured")
                .request()
                .header(HttpHeaders.AUTHORIZATION, 
                        getBasicAuthHeader("peeskillet", "secret"))
                .get();
        assertEquals(200, response.getStatus());
        assertEquals(USER_RESPONSE, response.readEntity(String.class));
    }
    
    @Test
    public void should_return_403_with_unauthorized_admin() {
        Response response = target("secured/adminSecured")
                .request()
                .header(HttpHeaders.AUTHORIZATION, 
                        getBasicAuthHeader("peeskillet", "secret"))
                .get();
        assertEquals(403, response.getStatus());
    }
    
    @Test
    public void should_return_200_response_with_authorized_admin() {
        Response response = target("secured/adminSecured")
                .request()
                .header(HttpHeaders.AUTHORIZATION, 
                        getBasicAuthHeader("stackoverflow", "superSecret"))
                .get();
        assertEquals(200, response.getStatus());
        assertEquals(ADMIN_RESPONSE, response.readEntity(String.class));
    }
}

Here is the only dependency needed to run the test

<dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>${jersey2.version}</version>
    <scope>test</scope>
</dependency>
like image 96
Paul Samsotha Avatar answered Oct 14 '22 09:10

Paul Samsotha