Search code examples
springoauthspring-securityjerseyjersey-2.0

Authorize User in Spring/Jersey


I'm working in a Spring/Jersey setup and trying to figure out how to authorize a user to access only resources that belong to them. I've seen numerous examples of how to secure an endpoint using authorization or broad role-based security, but I haven't seen anything that makes sure the user submitting the request is submitting it for his/her own stuff.

For clarity, suppose I have a GET /account/1 endpoint. I want only the user with ID 1 to be able to access that account information and only once he/she is authenticated. I've got the latter part taken care of using OAuth, but I can't figure out the former part.

How could I go about doing this?


Solution

  • By default, Spring Security's SecurityContext is a thread local. So you can access it anywhere from the request thread, using the SecurityContextHolder. From the SecurityContext you can obtain the UserDetails, which will have the user name.

    So because the Jersey request processing occurs in the same request thread, you should be able to access this information. From a Jersey component you can use whatever service you want to obtain you actual user domain objects and just check for the same user. For example you can have something like

    @Path("/accounts")
    public class AccountsResource {
    
        @Inject
        private UserService userService;
    
        @GET
        @Path("/{id}")
        @Produces("application/json")
        public User get(@PathParam("id") Long id) {
            User user = userService.getUser(id);
            // if user == null throw 404
    
            UserDetails userDetails
                    = (UserDetails) SecurityContextHolder.getContext()
                    .getAuthentication().getPrincipal();
    
            if (!userDetails.getUsername().equals(user.getUsername())) {
                throw new ForbiddenException();
            }
    
            return user;
        }
    }
    

    If you have a lot of endpoints like this, and you want to keep it DRY, you can use a Jersey filter to handle the access control process.

    @Provider
    @AccessFiltered
    @Priority(Priorities.AUTHORIZATION)
    public class UserAccessFilter implements ContainerRequestFilter {
    
        @Inject
        private UserService userService;
    
        @Override
        public void filter(ContainerRequestContext requestContext) {
            List<String> params = requestContext.getUriInfo().getPathParameters().get("id");
            Long id = Long.parseLong(params.get(0));
            User user = userService.getUser(id);
            // if user == null throw 404
    
            UserDetails userDetails
                    = (UserDetails) SecurityContextHolder.getContext()
                    .getAuthentication().getPrincipal();
    
            if (!userDetails.getUsername().equals(user.getUsername())) {
                throw new ForbiddenException();
            }
        }
    }
    

    Now with the Name Binding Annotation @AccessFiltered

    @NameBinding
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.TYPE})
    public @interface AccessFiltered {
    }
    

    We can just filter the methods we annotated

    @GET
    @Path("/{id}")
    @AccessFiltered
    @Produces("application/json")
    public User get(@PathParam("id") Long id) {
    }
    

    Now suppose we want access to that User, and we don't want to have to hit the DB again to retrieve it in our resource method. It's possible to put the User we obtained inside the filter, into the request context (a full example here)

    public class UserAccessFilter implements ContainerRequestFilter {
    
        public static final String USER_FILTER_PROPERTY = "UserAccessFilter.User";
    
        @Override
        public void filter(ContainerRequestContext requestContext) {
            ...
            User user = userService.getUser(id);
            ...
            requestContext.setProperty(USER_FILTER_PROPERTY, user);
        }
    }
    

    Then we can just inject the User into our resource method

    @GET
    @Path("/{id}")
    @AccessFiltered
    @Produces("application/json")
    public User get(@Context User user) {
        return user;
    }
    

    There's one more step to configure this (i.e. the Factory - see the previous link). You could also create a custom annotation for injecting the user, but that gets a bit more complicated.

    I'm sure this type of access control could be implemented using Spring Security's ACL feature, but for me being a Jersey user, this way is more intuitive for me.