No CSRF token provided on initial page load in production mode if index page is served as a public asset

Hi,

I have a strange issue with Play (2.8.5) not setting the CSRF protection related Set-Cookie header during initial loading of index.html page. Strangely this happens only when running Play in production mode. The index page is served as public asset and it’s just a plain html page so we’re not talking about twirl templates here. Route is configured like this:

GET  /  controllers.FrontendRouterController.index()

and the implementation is this:

import controllers.Assets
import play.api.{Environment, Mode}
import play.api.mvc.{Action, AnyContent, InjectedController}

import javax.inject.Inject

class FrontendRouterController @Inject()(assets: Assets, env: Environment)
    extends InjectedController {

  def index(): Action[AnyContent] = env.mode match {
    case Mode.Dev => assets.at("dev-index.html")
    case _ => assets.at("index.html")
  }

}

Now if I change the index method to return e.g. Ok("hello") then in the response I can see that there’s

Set-Cookie: XSRF-TOKEN=c66634c57521d3b362fe2a86a0b170ed357fe978-1613731879064-98752d2871f30dc02c98271a; SameSite=Lax; Path=/

but when using the built-in Assets controller to return the response then this header does not get added. Of course this leads to client not being able to provide the token in future requests. Is this some known issue with public assets that CSRF token is not inserted automatically?

Here’s the filter configuration:

play.filters.csrf.header.name = "X-XSRF-TOKEN"
play.filters.csrf.cookie.name = "XSRF-TOKEN"
play.filters.csrf.header.protectHeaders = null

The client is an Angular 8 single page app if that makes any difference. We used to serve the index page as a Play template like this: Ok(views.html.index.render()) but switched to this public asset approach recently because it works better with our frontend tooling (webpack mostly).

Okay, got some more information on what’s happening by turning on trace level logging:

2021-02-19 16:02:16,111 [trace] p.a.m.ActionBuilder$$anon$9 - Invoking action with request: GET /
2021-02-19 16:02:16,212 [trace] p.filters.CSRF - [CSRF] Not adding token to response that might get cached by a shared cache (e.g. proxies)

Seems reasonable. However few questions arise: How come this happens only in production mode? And more importantly, how to circumvent this problem?

–edit–

And I think I have it solved now having looked at CSRF filter sources. The result it examines needs to have for instance Cache-Control header set to “no-cache” for CSRF token to be added. Luckily we already have a filter for intercepting responses and this handling was easy to add there:

override def apply(next: RequestHeader => Future[mvc.Result])(
      rh: RequestHeader): Future[mvc.Result] = rh.path match {
    case "/" => next.apply(rh).map(_.withHeaders(("Cache-Control", "no-cache")))
    ...
  }

Very glad that this got solved even though it required diving into some trace-level logging :slight_smile: I hope that this helps someone else too.

How come this happens only in production mode?

Because in DEV mode caching is disabled by default. That makes sense, so you always see the latest changes when developing.

1 Like

Play might want to mention this somewhere in the CSRF documentation though? I don’t think that this is a rare use case given that people do use separated frontend apps with Play? And it is a nasty surprise after having no whatsoever problems until going prod mode.

Actually there are other (more elegant?) ways to solve your problem.
Instead of using af filter you can

play.assets.cache."/public/index.html"="no-cache"
// or for a folder
play.assets.cache."/public/html/"="no-cache"
// Actually I am not sure if quotation marks should be like this:
"play.assets.cache./public/index.html"="no-cache"

Play might want to mention this somewhere in the CSRF documentation though?

If you could provide a pull request that would be great,thanks!