How to Serve Public and Private Authenticated Assets via Play

I’m trying to serve public and private assets via a Play Assets controller but my authentication annotations do not get executed on the Asset request methods that need it. When I specify a route to call secureAt, it doesn’t execute the annotation. Does Play support annotations for Asset controller requests?

import play.api.Configuration;
import play.api.mvc.AnyContent;
import controllers.AssetsMetadata;
import play.api.http.HttpErrorHandler;
import controllers.Assets;
import play.api.mvc.Action;
import security.Authenticated;

import javax.inject.Inject;

public class CommonAssets extends Assets {

    private final String libPrefix;
    private final String commonPrefix;

    @Inject
    public CommonAssets(Configuration appConfig, HttpErrorHandler errorHandler, AssetsMetadata meta) {
        super(errorHandler, meta);
        this.libPrefix = "/public/lib";
        this.commonPrefix = appConfig.underlying().getString("assets.common.prefix");
    }

    public Action<AnyContent> at(String path, String file) {
        boolean aggressiveCaching = true;
        return at(commonPrefix + path, file, aggressiveCaching);
    }

    @Authenticated
    public Action<AnyContent> secureAt(String path, String file) {
        boolean aggressiveCaching = true;
        return at(commonPrefix + path, file, aggressiveCaching);
    }

    public Action<AnyContent> libAt(String path, String file) {
        boolean aggressiveCaching = true;
        return at(libPrefix + path, file, aggressiveCaching);
    }

    @Authenticated
    public Action<AnyContent> secureLibAt(String path, String file) {
        boolean aggressiveCaching = true;
        return at(libPrefix + path, file, aggressiveCaching);
    }
}

I think the problem here is a mix of the Java and Scala APIs. Annotations are supported on Java API action methods (which return Result) but not on Scala API Actions. The Assets class uses Scala Actions.

A fix here will provide some custom coding to link the two APIs together. Sorry I only have time for a quick answer. Maybe someone else will be able to help…

So if I converted these to Java API action methods would I lose any of the benefits (caching, etc) from using the Assets controller for serving assets?

To me it makes sense to use the Assets controller for handling assets but I only want authenticated users to be able to view certain assets. If I were to convert all assets from being served by the Assets controller to a Java API controller what would an example method look like since I would need to convert a Scala action returned by at to a Result.

@Authenticated
    public Result secureAt(String path, String file) {
        boolean aggressiveCaching = true;
        return assets.at(commonPrefix + path, file, aggressiveCaching);
    }

Hi @batman,

I can’t see why it isn’t working. How are your conf/routes files? Also, how are the views generating URLs to public and private assets? Keep in mind that, if you having the following structure:

public
|_ private/asset.jpg
|_ a.public.asset.jpg

And a route like:

GET  /assets/*file    controllers.Assets.versioned(path="/public", file: Asset)

Then the following URL will deliver private/asset.jpg: http://localhost:9000/assets/private/asset.jpg.What you may want to do is to have segregate public and private assets at directory level too, so that you can later segregate them at routes level.

Oh, well it IS serving the file. It’s just not requiring authentication to access it. The annotation is not getting executed. It completely ignores it.

My routes file looks like

GET         /docs/specification         controllers.common.CommonAssets.secureAt(path="/private/specification", file = "swagger.json")

My asset structure is like:

common
|- app
|---controllers
|---assets
|-----private
|--------specification
|----------swagger.json
|- conf

I don’t have a GET /assets/*file route in my route file because I don’t need it. I also don’t have any views that generate URLs. I’m directly navigating to http://localhost:9000/docs/specification and it serves the file just fine, but it should be failing authentication.

Hum, so, I think that @richdougherty suspicions may be correct here. Mixing Java and Scala APIs (returning Action<AnyContent> and using an annotation) is probably not working as you expect.

No, all the headers added by Assets.at method will be preserved. But remember to return a play.mvc.Result (Java) instead of a play.api.mvc.Result (Scala). So your code would be something like:

@Authenticated
public play.mvc.Result secureAt(String path, String file) {
    boolean aggressiveCaching = true;
    return assets.at(commonPrefix + path, file, aggressiveCaching).asJava();
}

Notice that there is a asJava there.

1 Like

Nice! I didn’t know about this conversion.

Except that the asJava() method in you example returns an play.mvc.EssentialAction and not a play.mvc.Result, so this doesn’t work. Is there a underlying method I can call to pass in an Action or EssentialAction that will return a play.mvc.Result?

Another option would be to write a filter and filter by URL. Not as elegant as having separate actions, but it might be easier.

How much work would it be to add support in Play for annotations to get executed for AssetBuilder requests? Or allow assets to be served from Controller classes? It seems like as long as my controller action methods return a Result or CompletionStage<Result>, it executes the annotations correctly, but not when it returns a Action<> or EssentialAction.

I would be willing to contribute to support this but I would need some pointers on where I should start looking to fix this.

I haven’t tested this, but you could probably do something like:

@Authenticated
public CompletionStage<Result> secureAt(String path, String file) {
  boolean aggressiveCaching = true;
  return assets.at(commonPrefix + path, file, aggressiveCaching)
    .asJava().apply(request()).run(materializer);
}

So, basically, run the Scala action to get its accumulator, materialize it (with a Materializer injected into your controller), and return the resulting Result. This is assuming there is no request body to process.

It would probably require some significant code changes, depending on how closely you wanted to mimic regular Java actions. The Scala API actions work at a lower level of abstraction than the Java actions. Java actions will assume an existing Http.Context, for example.

If you’re interested in how it works: there is a special instance of play.api.mvc.Action called JavaAction that wraps Java methods and does the necessary reflection to apply the annotation logic. This action is actually created by an instance of HandlerInvokerFactory provided implicitly based on the type of the route in the generated router.

2 Likes

Thanks a lot Greg. This actually worked. I feel like this is something that more and more people will utilize if they have a mix of public and private assets that they want only an authenticated client to see. Would love to see something more elegant get added to the Play framework, but in the mean time this will do for now. Thanks for the help.