DEV Community

Cover image for Web Services in Java 3 - Authorization Annotation
Jonas Funcke
Jonas Funcke

Posted on • Edited on

Web Services in Java 3 - Authorization Annotation

When providing APIs to the web, we sometimes (or most of the times) want to manage whos, whichs and wheres. Or in other words who can access which data where.

Some theory

That's where authorization comes into play. Some (especially me, until I googled it) think that authorization is the same thing as authentication, but behold: this is not the case.
When we talk about authentication, we mean the process where the client introduces itself to the provider.
For example: the part where we type in our username and password.
The result is most of the times some form of entry pass. For example a session, an API Key or something else.

Authorization on the other hand, handles which authenticated user can access what type of resource. A very common example for this process is a role system.

Let's get to work

Now that we know what we want to achieve, let's take a look at our Java code.

Java offers many ways to associate a so-called filter - a component, that gets executed around a request - with a route or a resource.
Many of these include a web.xml or similar complicated matters.
That's why we will focus on a annotation based approach:

What we want to achieve:

@Path("")
public class ExampleResource {
    @GET
    @Path("all")
    @Produces(MediaType.APPLICATION_JSON)
    @Authorization("example:read")
    public String getAll() throws Exception{
        return "{ \"message\": \"Hello World\"}";
    }
}

Enter fullscreen mode Exit fullscreen mode

The good thing is that most javax.ws.rs implementations already implement most of these features.
The only thing we will need to implement is the @Authorization annotation.

1. The annotation interface

To create an annotation, we need an interface declaring it. This looks like this:

// Retention declares the time of evaluation.
// As we want to have it evaluated at runtime, we declare it runtime.
@Retention(RUNTIME)
@Target({TYPE, METHOD}) // This annotation specifies where this annotation can be used.
public @interface Authorization {
    /**
     * List of roles that are permitted access.
     */
    String[] value();
}
Enter fullscreen mode Exit fullscreen mode

2. Annotation Handler Class

Okay, now we have an annotation. ...but it does nothing at all right now.
That's why we need to declare a filter to handle annotated resources:

/**
 * When deploying your application as .war or .jar into a web server,
 * this annotation declares the class as provider for a filter.
 */
@Provider
public class AuthorizationFilter implements ContainerRequestFilter
{
     @Override
     public void filter(ContainerRequestContext requestContext) {
         // TODO: Fill
     }
}
Enter fullscreen mode Exit fullscreen mode

Our filter implements the ContainerRequestFilter interface from the Java REST Services extension. This interface requires a method called 'filter'.
This method will be called, when our filter is being applied to the request. It is being passed an instance of ContainerRequestContext, which provides metadata about the request.
For more information, check out the Official JavaDoc!

Our filter will handle authorization in its filter method.
But at first we need to check if the called resource is decorated with our annotation. To achieve this, we will provide our class with the ResourceInfo.
This is an object which holds information about the Java method, that is being evaluated in this request call.

public class AuthorizationFilter implements ContainerRequestFilter
{
    // Provides information about the called resource
    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) {
        // TODO: Fill
    }
}
Enter fullscreen mode Exit fullscreen mode

The @Context annotation tells the server to provide this class with the corresponding instance of RequestContext automatically. So we don't have to worry about how this information gets here. Neat!

To keep the used identifiers consistent and to make future changes easier, we should also declare constant variables with our header and our scheme:

...
    // Provides information about the called resource
    @Context
    private ResourceInfo resourceInfo;
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORIZATION_SCHEME = "Bearer";

    @Override
    public void filter(ContainerRequestContext requestContext) {
        // TODO: Fill
    }
...
Enter fullscreen mode Exit fullscreen mode

Nice! Now let's get to work:

At first we need to check for the annotation:

    ...
    @Override
    public void filter(ContainerRequestContext requestContext) {
        Method calledMethod = resourceInfo.getResourceMethod();
        if(calledMethod.isAnnotationPresent(Authorization.class) {
           // Handle authorization
        }
    }
    ...
Enter fullscreen mode Exit fullscreen mode

Now that we know that our annotation is present, we need to check if the authorization header in our request is present. If not, we can just stop processing the request and return a 403.

    ...
    if(calledMethod.isAnnotationPresent(Authorization.class) {
        String authorization = requestContext.getHeaderString(AUTHORIZATION_HEADER);
        //If no authorization information present; block access
        if(authorization == null || authorization.isEmpty())
        {
           throw new ForbiddenException("Resource Forbidden");
        }
    }
    ...
Enter fullscreen mode Exit fullscreen mode
  • Annotation: Present
  • Header: Filled
  • Access Requirements: not yet fetched

We will do this by accessing the array of Strings we passed to the annotation.

    ...
    if(calledMethod.isAnnotationPresent(Authorization.class) {
        ...
        Authorization authAnnotation = calledMethod.getAnnotation(Authorization.class);
        Set<String> accessRequirements = new HashSet<>(Arrays.asList(authAnnotation.value()));
    }
    ...
Enter fullscreen mode Exit fullscreen mode

Ok. In the incoming request, the authorization header is formatted in the following way:

Header Name Header Value
Authorization Bearer

This means, that our token is being preceded by the word Bearer. So we need to split the content of the authorization header to access the token:

    ...
    if(calledMethod.isAnnotationPresent(Authorization.class) {
        ...
        Authorization authAnnotation = calledMethod.getAnnotation(Authorization.class);
        Set<String> accessRequirements = new HashSet<>(Arrays.asList(authAnnotation.value()));
        final String key = authorization.replaceFirst(AUTHENTICATION_SCHEME + " ", "");
    }
    ...
Enter fullscreen mode Exit fullscreen mode

Alrighty! Now that we have the token we need to check the access rights assigned to it. Normally we would call some form of data storage or something similar at this point. This really is out of the boundaries of this post though.

That's why we will implement a static map with our user data.

    // Provides information about the called resource
    @Context
    private ResourceInfo resourceInfo;
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORIZATION_SCHEME = "Bearer";

    private static final Map<String, Set<String>> userData = new HashMap<String, Set<String>>() {{
        put("admin", new HashSet<String>(){{
            add("example:read");
        }});
        put("example_user", new HashSet<String>() {{
            add("example:write");
        }});
    }};

    @Override
    public void filter(ContainerRequestContext requestContext) {
        ...
    }
...
Enter fullscreen mode Exit fullscreen mode

This will give us two tokens. One with the right to read in our example resource and one with the right to write the example resource.
For the endpoint we have at the beginning of this post, we specified example:read as access requirement. This means, that only the admin user can access the secured resource.

Token Access Rights
admin [example:read]
example_user [example:write]

Now we should check, if the sent token has the correct access rights. To make our code more readable, we should really put this check in its own method:

...
    private boolean authorize(final String key, final Set<String> accessRights)
    {
        boolean isAllowed = false;
        Set<String> permittedActions = userData.get(key);

        if(permittedActions != null && permittedActions.stream().anyMatch(accessRights::contains))
            isAllowed = true;

        return isAllowed;
    }
}
Enter fullscreen mode Exit fullscreen mode

And check for its result

...
    Set<String> accessRequirements = new HashSet<>(Arrays.asList(authAnnotation.value()));
    final String key = authorization.replaceFirst(AUTHENTICATION_SCHEME + " ", "");
    if(!authorized(key, acessRequirements))
        throw new ForbiddenException("Resource forbidden");
}
Enter fullscreen mode Exit fullscreen mode

That's it! That's our authorization handling. If you want the complete code, check out this gist.

Now that we have our annotation and our annotation handling, we need to provide our solution to the server.
As this is part of a series, I will refer to the type of server we implemented in Part 1.
In this case, we just add it to the resources declared in our resource config:

// Main.java
...
ResourceConfig resourceConfig = new ResourceConfig(); 
resourceConfig.register(ExampleResource.class); // our rest resource
// the parser for JSON and XML request bodies
resourceConfig.register(JacksonFeature.class);
resourceConfig.register(AuthorizationFilter.class);
Enter fullscreen mode Exit fullscreen mode

Now we have a way to secure our API.
Enjoy!

Image Credits:
Photo by Siarhei Horbach on Unsplash

Top comments (0)