How to Secure APIs in the Play Framework

play framework

At Logz.io, we’re using Play 2.0 framework. We wanted to share our experience hardening and securing this part of our architecture by using action composition.

 

Let’s jump to the gory details.

Action composition is a powerful way to enhance or restrict a behavior of a controller.

By composing actions, we practically intercept the incoming request before it arrives at the controller. This allows us to perform additional work on the incoming request before the actual process.

Action in Play 2.0 Java version, is an instance of play.mvc.Action with a method named call(Context context) that returns a play.mvc.Result value (or better to say Promise results):

public abstract class Action extends Results {
    public abstract F.Promise call(Context var1) throws Throwable;
}

Play, behind the scenes, adds a root action that will properly use the call method above, and this allows us to further compose Actions by ourselves.

Action composition allows us to add one or more behaviors to other actions, for example, we can compose an Authentication action on another action (service) to limit the access to authenticated users only.

Action composition is done using Java annotations:

Java annotations are a form of Metadata for code, and are used to decorate a class, method, field, parameter, variable, constructor, or package. Annotations can be used at compile time or retrieved at runtime using reflection.

Building our own custom actions will be done using annotations declared for runtime (RetentionPolicy.Runtime):

1.  Define an annotation – this is the annotation we use to mark an action composition over another action:

@With({Access.AuthenticatedAction.class})
@Target({ElementType.TYPE, ElementType.METHOD}) 
@Retention(RetentionPolicy.RUNTIME) 
public @interface Authenticated { boolean value() default true; }

Annotation declaration is similar to an interface declaration, forwarded by @. Annotations only define some information on the element they decorate, and they do not do any business logic. For this we need a consumer to act upon the presence of annotations.

Defining a custom annotation also uses other annotations: @Target which defines the allowed elements to use with this annotation, @Retention which defines when is this annotation is expected to be used (source, class, or runtime).

In order to define a custom action, Play provides the @With annotation to declare which class (must be of type extending Action) to use to perform the actual composite work (a.k.a the consumer).

Inside the annotation declaration, we can only use primitives, string and enums. All members are defined as methods and can be added with the default value.

When using the annotation to decorate another Action, we can set values for each member using the name=value format. If only one member is defined, it must be named value and can be set without the attribute name:

@Access.Authenticated(true)
public static Result summary() { }

In case we defined a default value, we can use the decoration without any parameter.

2.  Define a consumer – a class which extends Action and implements specific logic inside the call method:

public static class AuthenticatedAction extends Action { 
    public AuthenticatedAction() { } 
    public F.Promise call(Http.Context context) throws Throwable { 
        return this.delegate.call(context); 
    }
}

The Action class retrieves the custom annotation as configuration, and the Action it wraps will be set as its delegate

public abstract class Action extends Results { 
    public T configuration; 
    public Action<?> delegate; 
}

3. This allows us to implement the call method and inside forward work to the original Action using the delegate. We can also access the annotation members using the configuration member of the abstract Action class. We will now enforce the restrictions verifying the access to this API service is done by an authenticated user only. If it is, we can forward work using the delegate, if not – we can return HTTP Error 401 Unauthorized:

public class Access {
 
    public static class AuthenticatedAction extends Action<Authenticated> {
 
        public F.Promise<Result> call(Http.Context context) throws Throwable {
 
            // whether we need to log this - default is YES
            boolean logFailure = (this.configuration).value();
 
            // our validation is based on a token sent inside HTTP Header
            // we can get access to it through the context provided as input for this method
            String authToken = context.request().getHeader("USER-AUTH-TOKEN");
 
            // if no token is found int the header - we restrict the access
            if (authToken == null) {
                if (logFailure) {
                    Logger.info("authentication failed with context: {}", context);
                }
                return F.Promise.pure(unauthorized());
            }
 
            // let's find the username (assuming we find it)
            String username = this.getUsername(authToken);
 
            // now we can put into the context which will be available from the controller to use
            // we do this using the args member of context
            context.args.put("username", username);
 
            // here we can pass work to the wrapped Action with the same context
            return this.delegate.call(context);
        }
 
        private String getUsername(String token) {
            // do some logic to retrieve the username by auth token ...
            return token + "SOME NAME";
        }
    }
 
    @With({Access.AuthenticatedAction.class})
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Authenticated {
        boolean value() default true;
    }
}

Usage in the controller:

@Access.Authenticated
public static Result summary() { }

In order to pass object/s from the Action to the controller, you can use the args member of context, in this example – who is the username currently logged in. This context is then available to the controller to get the data out:

String username = (String) ctx().args.get("username");

We use action composition extensively throughout our Play code and find it useful. Hopefully, this helps!

Power your DevOps Initiatives with Logz.io's Machine Learning Features!

Artboard Created with Sketch.
× Book time with us at re:Invent here! Book