[java] Best practice for REST token-based authentication with JAX-RS and Jersey

This answer is all about authorization and it is a complement of my previous answer about authentication

Why another answer? I attempted to expand my previous answer by adding details on how to support JSR-250 annotations. However the original answer became the way too long and exceeded the maximum length of 30,000 characters. So I moved the whole authorization details to this answer, keeping the other answer focused on performing authentication and issuing tokens.


Supporting role-based authorization with the @Secured annotation

Besides authentication flow shown in the other answer, role-based authorization can be supported in the REST endpoints.

Create an enumeration and define the roles according to your needs:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

Change the @Secured name binding annotation created before to support roles:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
    Role[] value() default {};
}

And then annotate the resource classes and methods with @Secured to perform the authorization. The method annotations will override the class annotations:

@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @Secured
        // But it's declared within a class annotated with @Secured({Role.ROLE_1})
        // So it only can be executed by the users who have the ROLE_1 role
        ...
    }

    @DELETE
    @Path("{id}")    
    @Produces(MediaType.APPLICATION_JSON)
    @Secured({Role.ROLE_1, Role.ROLE_2})
    public Response myOtherMethod(@PathParam("id") Long id) {
        // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
        // The method annotation overrides the class annotation
        // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
        ...
    }
}

Create a filter with the AUTHORIZATION priority, which is executed after the AUTHENTICATION priority filter defined previously.

The ResourceInfo can be used to get the resource Method and resource Class that will handle the request and then extract the @Secured annotations from them:

@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        // Get the resource class which matches with the requested URL
        // Extract the roles declared by it
        Class<?> resourceClass = resourceInfo.getResourceClass();
        List<Role> classRoles = extractRoles(resourceClass);

        // Get the resource method which matches with the requested URL
        // Extract the roles declared by it
        Method resourceMethod = resourceInfo.getResourceMethod();
        List<Role> methodRoles = extractRoles(resourceMethod);

        try {

            // Check if the user is allowed to execute the method
            // The method annotations override the class annotations
            if (methodRoles.isEmpty()) {
                checkPermissions(classRoles);
            } else {
                checkPermissions(methodRoles);
            }

        } catch (Exception e) {
            requestContext.abortWith(
                Response.status(Response.Status.FORBIDDEN).build());
        }
    }

    // Extract the roles from the annotated element
    private List<Role> extractRoles(AnnotatedElement annotatedElement) {
        if (annotatedElement == null) {
            return new ArrayList<Role>();
        } else {
            Secured secured = annotatedElement.getAnnotation(Secured.class);
            if (secured == null) {
                return new ArrayList<Role>();
            } else {
                Role[] allowedRoles = secured.value();
                return Arrays.asList(allowedRoles);
            }
        }
    }

    private void checkPermissions(List<Role> allowedRoles) throws Exception {
        // Check if the user contains one of the allowed roles
        // Throw an Exception if the user has not permission to execute the method
    }
}

If the user has no permission to execute the operation, the request is aborted with a 403 (Forbidden).

To know the user who is performing the request, see my previous answer. You can get it from the SecurityContext (which should be already set in the ContainerRequestContext) or inject it using CDI, depending on the approach you go for.

If a @Secured annotation has no roles declared, you can assume all authenticated users can access that endpoint, disregarding the roles the users have.

Supporting role-based authorization with JSR-250 annotations

Alternatively to defining the roles in the @Secured annotation as shown above, you could consider JSR-250 annotations such as @RolesAllowed, @PermitAll and @DenyAll.

JAX-RS doesn't support such annotations out-of-the-box, but it could be achieved with a filter. Here are a few considerations to keep in mind if you want to support all of them:

So an authorization filter that checks JSR-250 annotations could be like:

@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        Method method = resourceInfo.getResourceMethod();

        // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
        if (method.isAnnotationPresent(DenyAll.class)) {
            refuseRequest();
        }

        // @RolesAllowed on the method takes precedence over @PermitAll
        RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
            return;
        }

        // @PermitAll on the method takes precedence over @RolesAllowed on the class
        if (method.isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // @DenyAll can't be attached to classes

        // @RolesAllowed on the class takes precedence over @PermitAll on the class
        rolesAllowed = 
            resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
        }

        // @PermitAll on the class
        if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // Authentication is required for non-annotated methods
        if (!isAuthenticated(requestContext)) {
            refuseRequest();
        }
    }

    /**
     * Perform authorization based on roles.
     *
     * @param rolesAllowed
     * @param requestContext
     */
    private void performAuthorization(String[] rolesAllowed, 
                                      ContainerRequestContext requestContext) {

        if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
            refuseRequest();
        }

        for (final String role : rolesAllowed) {
            if (requestContext.getSecurityContext().isUserInRole(role)) {
                return;
            }
        }

        refuseRequest();
    }

    /**
     * Check if the user is authenticated.
     *
     * @param requestContext
     * @return
     */
    private boolean isAuthenticated(final ContainerRequestContext requestContext) {
        // Return true if the user is authenticated or false otherwise
        // An implementation could be like:
        // return requestContext.getSecurityContext().getUserPrincipal() != null;
    }

    /**
     * Refuse the request.
     */
    private void refuseRequest() {
        throw new AccessDeniedException(
            "You don't have permissions to perform this action.");
    }
}

Note: The above implementation is based on the Jersey RolesAllowedDynamicFeature. If you use Jersey, you don't need to write your own filter, just use the existing implementation.

Examples related to java

Under what circumstances can I call findViewById with an Options Menu / Action Bar item? How much should a function trust another function How to implement a simple scenario the OO way Two constructors How do I get some variable from another class in Java? this in equals method How to split a string in two and store it in a field How to do perspective fixing? String index out of range: 4 My eclipse won't open, i download the bundle pack it keeps saying error log

Examples related to rest

Access blocked by CORS policy: Response to preflight request doesn't pass access control check Returning data from Axios API Access Control Origin Header error using Axios in React Web throwing error in Chrome JSON parse error: Can not construct instance of java.time.LocalDate: no String-argument constructor/factory method to deserialize from String value How to send json data in POST request using C# How to enable CORS in ASP.net Core WebAPI RestClientException: Could not extract response. no suitable HttpMessageConverter found REST API - Use the "Accept: application/json" HTTP Header 'Field required a bean of type that could not be found.' error spring restful API using mongodb MultipartException: Current request is not a multipart request

Examples related to authentication

Set cookies for cross origin requests How Spring Security Filter Chain works What are the main differences between JWT and OAuth authentication? http post - how to send Authorization header? ASP.NET Core Web API Authentication Token based authentication in Web API without any user interface Custom Authentication in ASP.Net-Core Basic Authentication Using JavaScript Adding ASP.NET MVC5 Identity Authentication to an existing project LDAP: error code 49 - 80090308: LdapErr: DSID-0C0903A9, comment: AcceptSecurityContext error, data 52e, v1db1

Examples related to jax-rs

Java 8 LocalDate Jackson format File upload along with other object in Jersey restful web service Best practice for REST token-based authentication with JAX-RS and Jersey MessageBodyWriter not found for media type=application/json Can not deserialize instance of java.util.ArrayList out of START_OBJECT token Read response body in JAX-RS client from a post request What is the difference between JAX-RS and JAX-WS? javax.xml.bind.JAXBException: Class *** nor any of its super class is known to this context When to use @QueryParam vs @PathParam How to set up JAX-RS Application using annotations only (no web.xml)?

Examples related to jersey-2.0

Jersey stopped working with InjectionManagerFactory not found Best practice for REST token-based authentication with JAX-RS and Jersey java.lang.ClassNotFoundException: com.sun.jersey.spi.container.servlet.ServletContainer Dependency injection with Jersey 2.0