5

EDIT: I just realized, is it even possible to perform a custom action with a custom attribute in Java? Or is it just informational?

I want to include an authentication token in my Jax-RS service header, but I don't want to add a parameter to every request to get the header and check it like so:

public Response getUser(@Context HttpHeaders headers) {
    if(authorize(headers.getRequestHeader("token").get(0)) {
        // Do something
    }
}

I would much rather add an attribute to each request (or even the class if that is possible:

@Authorize
public Response getUser() {
    // Do something
}

This way I can also add the attribute to only the requests I want to.

And if the request isn't authorized, I can override it and return a 401.

A custom attribute is easy to write, but how can I get the header information in the attribute without passing it in every time?

NOTE: I would rather not use a web.xml. I don't have one right now and I don't like using them. I want to keep my code clean without xml and I think if I used a filter/web.xml it would apply to all calls. If that is the only way, I will, but I much prefer the approach with custom attributes.

3 Answers3

2

"I think if I used a filter/web.xml it would apply to all calls"

Actually there are @NameBinding annotations we can use. For example

@NameBinding
@Rentention(RetentionPoilicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Authorize {}

Then just annotate the filter and the methods/classes you want filtered.

@Authorize
public Response getUser() {
    // Do something
}

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

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

        MultivauledMap<String, String> headers - requestContext.getHeaders();
        ...
        if (!authorized) {
            throw new NotAuthorizedException();
        }
    }
}

Notice the use of @Priority. This is important. Say you want the authenticate also, so you create a filter for authentication. If you don't set the priority, either filter may occur first. It's unpredictable. If we provide the authentication filter with @Priority(Priorities.AUTHENTICATION), then that filter will always occur before the @Priority(Priorities.AUTHORIZATION) filter.

You will also need to register this filter with the Application subclass (See some other Deployment Options (Jersey, but the Application subclass is portable with implementations))

@ApplicationPath("/api")
public class YourApplication extends Application {
    private Set<Class<?>> classes = new HashSet<>();
    private Set<Object> singletons = new HashSet<>();

    public YourApplication() {
        classes.add(AuthorizationRequestFilter.class);
    }
    @Override
    public Set<Class<?>> getClasses() {
        return classes;
    }
    @Override
    public Set<Object> singletons() {
        return singletons;
    }
}

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • This worked perfectly! Thanks. I am going to edit your response a little bit to show the final code, but the concept was exactly what I wanted. –  Nov 30 '14 at 01:14
  • I edited the response. If some admins could peer review it so the working code shows up, that would be awesome. –  Nov 30 '14 at 01:25
  • I'll do that. Yours has spelling errors and doesn't quite work, which is why I slightly edited it. But that's fine. –  Nov 30 '14 at 21:37
  • Actually, `@Authorize` annotation is redundant in your example. When you have your authentication filter annotated by `@Provider`, it would be automatically applied to all resources, even those not annotated by `@Authorize`. If you want to control, to which resources authorization is applied, using this custom annotation, you might want to remove `@Provider` annotation and register the filter manually in your `@ApplicationPath` class. Tested on Jersey 2.19. – jFrenetic Jul 15 '15 at 17:38
1

The best way to solve your use case would be to use name binding and filters. In this way, you can use a filter to do your authorization logic as well as return a 401 in the case of unauthorized requests.

You can find more information here.

Name binding via annotations is only supported as part of the Server API. In name binding, a name-binding annotation is first defined using the @NameBinding meta-annotation:

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

The defined name-binding annotation is then used to decorate a filter or interceptor class (more than one filter or interceptor may be decorated with the same name-binding annotation):
  @Logged
  public class LoggingFilter
          implements ContainerRequestFilter, ContainerResponseFilter {
      ...
  }

At last, the name-binding annotation is applied to the resource method(s) to which the name-bound JAX-RS provider(s) should be bound to:
  @Path("/")
  public class MyResourceClass {
      @GET
      @Produces("text/plain")
      @Path("{name}")
      @Logged
      public String hello(@PathParam("name") String name) {
          return "Hello " + name;
      }
  }

A name-binding annotation may also be attached to a custom JAX-RS Application subclass. In such case a name-bound JAX-RS provider bound by the annotation will be applied to all resource and sub-resource methods in the JAX-RS application:
  @Logged
  @ApplicationPath("myApp")
  public class MyApplication extends javax.ws.rs.core.Application {
      ...
}
Nandana
  • 1,240
  • 8
  • 17
1

Based on peeskillet's answer, which the concept is right but the code is slightly wrong, this is the final code for the answer.

Using @NameBinding, this works:

Authorize.java

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

}

AuthorizeFilter.java

note: still needs to do the actual token authorization. This is just checking if the token exists right now.

@Provider
@Authorize
@Priority(Priorities.AUTHORIZATION)
public class AuthorizeFilter implements ContainerRequestFilter
{
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException
    {
        MultivaluedMap<String, String> headers = requestContext.getHeaders();

        String token = headers.getFirst("token");

        if (token == null || token.isEmpty()) {
            Response.ResponseBuilder responseBuilder = Response
                    .status(Response.Status.UNAUTHORIZED)
                    .type(MediaType.APPLICATION_JSON)
                    .header("Access-Control-Allow-Origin", "*")
                    .header("Access-Control-Allow-Credentials", "true")
                    .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD")
                    .header("Access-Control-Max-Age", "1209600");

            requestContext.abortWith(responseBuilder.build());
        }
    }
}

ApplicationConfig.java

note: add the filter here so it doesn't have to be included in the web.xml

@ApplicationScoped 
@ApplicationPath("/api")
public class ApplicationConfig extends Application
{
    @Override
    public Set<Class<?>> getClasses()
    {
        return getRestResourceClasses();
    }

    private Set<Class<?>> getRestResourceClasses()
    {
        Set<Class<?>> resources = new java.util.HashSet<Class<?>>();
        resources.add(com.example.AuthorizeFilter.class);
        resources.add(com.example.UserService.class);
        return resources;
    }
}