ayesh/stateless-csrf

Secret-key based state-less CSRF token generator and validator for PHP 8. State-less means you do not have to store the CSRF token in session or database.

v1.4.1 2024-03-15 06:09 UTC

This package is auto-updated.

Last update: 2024-04-15 06:24:26 UTC


README

Packagist license CI

ayesh/stateless-csrf is PHP library that generates and validates stateless CSRF-protection tokens. This means the generated tokens are not stored in a database or disk on the server. Instead, a combination of a secret key (that only the server knows) and clues to identify a browser are used.

Because we do not store the generated tokens, this library does not provide protection against replay attacks.

Requirements

  • PHP 7.4 or later.
  • PHP built-in extensions: Hash and JSON (available unless PHP is compiled manually without these extensions).

Features

  • Generate a secure token using SHA-256 HMAC.
  • Optionally set an expiration time to tokens.
  • Generated tokens are URL-safe.
  • Optimized to be used with an Inversion of Control container.
  • Token validation is time-attack safe.

Installation

Copy-pasta the following in your terminal:

composer require ayesh/stateless-csrf

Examples

Simple Example without variables

Before the library can generate tokens, it must be fed with a secret key. This key can be a string of any length, and is used as the key in HMAC operations.

<?php 

use Ayesh\StatelessCSRF\StatelessCSRF;

$csrf_generator = new StatelessCSRF('your-secret-key-here');
$token = $csrf_generator->getToken('unique-id-for-key');

$csrf_generator->validate('unique-id-for-key', $token);

Above is the simplest example. First, we initialize the CSRF token generator with a secret key (your-secret-key-here). Any StatelessCSRF instance instantiate with the same secret key will be able to valdiate tokens generated by the other.

In an ideal use case, you will not be doing things like this. This library is meant to be used with an IoC container. Generate a single StatelessCSRF instance, and use it to generate as many as tokens needed. In subsequent requests, new StatelessCSRF instances (which are generated with the same secret) will be able to valdiate them. See the example at the bottom of this README for more elaborate examples.

Example with token expiration.

Because this library does not provide replay attack protection, an expiration time for the tokens makes more sense. The expiration time of the token is provided at the time the is generated. In the getToken method, set the second parameter to the UNIX timestamp when the token should expire.

<?php 

use Ayesh\StatelessCSRF\StatelessCSRF;

$csrf_generator = new StatelessCSRF('your-secret-key-here');
$token = $csrf_generator->getToken('unique-id-for-key', time() + 3600); // Expires in an hour.

$csrf_generator->validate('unique-id-for-key', $token);

In the validate(), you can provide the current time as the third parameter. If not provided, the token expiration timestamp will be compared against the current system time (what time() returns).

The expiration time is signed, so an attacker cannot change the timestamp and bypass the expiration.

Juicy example with user-agent, IP address, etc validation.

Although a secret-key based token combined with an expiration time provides good protection, you can make things more strict with user-agent string and IP address validation. For the library, it just needs to be fed with same "glue" values at both generation and validation stages.

You can use any value that uniquely identifies a user. User-agent string provided by the user and peer IP address are two great examples.

<?php 

use Ayesh\StatelessCSRF\StatelessCSRF;

$csrf_generator = new StatelessCSRF('your-secret-key-here');
$csrf_generator->setGlueData('ip', $_SERVER['REMOTE_ADDR']);
$csrf_generator->setGlueData('user-agent', $_SERVER['HTTP_USER_AGENT']);

$token = $csrf_generator->getToken('unique-id-for-key', time() + 3600); // Expires in an hour.

$csrf_generator->validate('unique-id-for-key', $token);

In the snippet above, ip and user-agent are arbitrary values. You can add any number of glue values. Calling setGlueData on the same key twice will overwrite the old value.

In the validator instance, the same set of glue values must be set, and set in the same order. I intentionally left out the glue value sorting to encourage callers to use some sort of container to get the StatelessCSRF instance instead of creating one everytime a token needs to be validated.

Example with Slim PHP Framework

use Ayesh\StatelessCSRF\StatelessCSRF;

$container['csrf'] = static function (Container $container): StatelessCSRF {
	$settings = $container->get('settings');
	$csrf = new StatelessCSRF($settings['secret_key']);
	$request = $container->get('request');
	$csrf->setGlueData('user_agent', $request->getHeaderLine('user-agent'));

	$server = $request->getServerParams();
	$csrf->setGlueData('ip', $server['REMOTE_ADDR']);

	return $csrf;
};

Now, from the container, you can fetch the StatelessCSRF instance from $container['csrf'], and it will be ready to be used with getToken and validate() calls.

Contribute

Contributions are welcome. Please feel to open an issue and/or send PRs along. For PRs, I appreciate the appropritate test coverage as well.