CSRF in “Headless” Applications
A similar question was asked in the Craft Slack today, so I thought I'd transcribe the consensus:

Internally, CSRF tokens come from a call to Craft::$app-&gt;getRequest()-&gt;getCsrfToken(). This means that any controller (or template, for that matter—more often handled by the {{ csrfInput() }} shortcut method) can grab a CSRF token at will.

In the original post, I'd noted that the users/get-remaining-session-time action returns a token—that token comes from the above call, so it certainly sounds like a valid built-in method for grabbing tokens.

That said, it does have some potential side effects related to the session (aside from refreshing the CSRF token).

Ultimately—provided the time—it seems like the recommended method is to create a custom controller and call the method explicitly.

As far as the client-side handling… if your application has a means to consolidate AJAX calls, it'd be worth automating the refresh and inclusion of CSRF tokens as HTTP headers. For example, before a regular POST request is initiated, the client might first ensure it has a fresh CSRF token, then issue the request with the new token.

Hope this helps anyone else getting started with client-rendered applications!


  Edit: As of Craft 3.1.13, you get a CSRF token in the JSON response when a user logs in (assuming the login occurs over Ajax)!