How to access Jersey resource secured by @RolesAllowed

13,195

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:

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>
Share:
13,195
Tom Sebastian
Author by

Tom Sebastian

Updated on June 24, 2022

Comments

  • Tom Sebastian
    Tom Sebastian almost 2 years

    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.

  • Tom Sebastian
    Tom Sebastian over 8 years
    thanks for the information. In my case the webservice is developed and deployed by another team, I have read only access to the src from where I got the annotation as specified in question. My doubt is while accessing this resource via restclient, how we will give the role information with the request.As u mentioned will it prompt for Role?
  • Paul Samsotha
    Paul Samsotha over 8 years
    If the rest services are developed by another team, then you need to ask them what the authentication scheme is. There's no one way to do authentication. If they are using Basic auth, then you should set Authorization header to Basic <base64(username:password)>. If it is something else, then you need to ask them how to send it. There is no way for us to know.
  • Paul Samsotha
    Paul Samsotha over 8 years
    In General the client should not be sending the role. The client sends the credentials, the server side looks up the client credentials and authenticates, then sets the SecurityContext, getting the role from the data store lookup of the user. The role should be associated with the user in the data store
  • Paul Samsotha
    Paul Samsotha over 8 years
    If you look at my tests at the bottom, you will see that all the requests only send the username and password. The request goes through the filter, and the user is lookedup in the UserStore, where the User is associated with a role.
  • Tom Sebastian
    Tom Sebastian over 8 years
    ok @peeskillet.Your previous comment is right, client no need to send the role only credentials need to be passed. I need to learn more about REST security . Thanks for your inputs .Better I shall ask the developers
  • Stefan
    Stefan almost 7 years
    @peeskillet I was wondering why my jersey application isn't calling the SecurityContext.isUserInRole() after setting it? I'm using my own code but I can't really find a huge difference between your example code I'm pretty much doing the same.
  • Paul Samsotha
    Paul Samsotha almost 7 years
    @klokklok DId you register the RolesAllowedDynamicFeature?