CSRF protection ^2.0
This plugin adds cross-site request forgery (CSRF) protection for your endpoints, utilising ProcessWire’s built-in token validation.
The plugin hooks into every POST, PUT, PATCH, or DELETE request and expects a valid token to be included in the request payload. The response of any successful request of those types includes the current token. If validation fails, an ApiException is thrown with the current token included.
The plugin also exposes a /csrf-token endpoint, which can be used to retrieve the current token.
Installation
Install the plugin by calling the addPlugin() method of the Api instance.
use PwJsonApi\Plugins\CSRFPlugin;
$api->addPlugin(new CSRFPlugin());Configuration
You can configure the plugin via the setup callback:
$api->addPlugin(new CSRFPlugin(), function ($plugin) {
// Token name
$plugin->tokenName = 'pw_json_api_csrf_token';
// Key name for the token in responses
$plugin->tokenKey = 'csrf_token';
// Endpoint path for retrieving the current token
$plugin->endpointPath = '/csrf-token';
// Replace the token after every successful POST, PUT, PATCH, or DELETE
// request (default: false)
$plugin->rotateToken = false;
});Token rotation ^2.5
By default, the token stays the same for the whole session. ProcessWire replaces it when the user logs in or out, and the new token is included in the login and logout responses.
When rotateToken is enabled, the token is replaced after every successful POST, PUT, PATCH, or DELETE request. The previous token stops working immediately, so concurrent requests sent with the same token fail. Enable rotation if responses that include the token are compressed and also reflect user-provided input, and your session cookie is not protected by SameSite. This combination exposes a constant token to BREACH attacks.
Configuring the token endpoint ^2.5
The token endpoint follows the authentication and authorization rules of the API instance, like any other endpoint. If the API instance has an authenticator, guests cannot retrieve a token — for example, before logging in.
Use setupService() to configure the service that provides the token endpoint. The setup function receives the service, so you can opt it out of the API rules:
$api->authenticate(new ExampleAuth());
$api->addPlugin(new CSRFPlugin(), function ($plugin) {
$plugin->setupService(function ($service) {
// Guests can retrieve a token
$service->skipAuthentication();
});
});If the API instance also has authorization callbacks, call skipAuthorization() as well.
TIP
If a request fails the token validation, the error response also includes the current token.
Front-end implementation
Front-end implementation is outside the scope of this guide, but here is a basic flow:
- Implement a way to store the current token (e.g. in memory, or a state manager).
- Before any
POST,PUT,PATCH, orDELETErequest, check whether a current token is available. If not, query the/csrf-tokenendpoint to retrieve and store it. - Pass the token as a header
- If the response is successful, read the token from the response using the
csrf_tokenkey and update your store. The token changes when the user logs in or out, and after every request if rotation is enabled.
let token = null;
// Fetch the initial token
async function fetchToken() {
const res = await fetch('/api/csrf-token');
const json = await res.json();
if (json.csrf_token) {
token = json.csrf_token;
}
}
// Make a POST request with the token, then store the returned token
async function post(url, body) {
if (!token) {
await fetchToken();
}
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[`X-${token.name}`]: token.value,
},
body: JSON.stringify(body),
});
const json = await res.json();
// Store the returned token
if (json.csrf_token) {
token = json.csrf_token;
}
return json;
}
const result = await post('/api/my-endpoint', { foo: 'bar' });