Search code examples
phptokencsrf

Single use CSRF token generation and validation for cross server communication in PHP


I have searched a lot trying to find something for my purpose, however most solutions revolve around CSRF tokens that work in conjunction with session data. My purpose requires "time based" token for cross server communication.

I have Server A that needs to receive and validate a token that is sent to it via POST from Server B. The token needs to be generated on Server B by hashing with a secret key. Server A has to validate the same. Now, the problem is that token needs to be limited to single-use (possibly?) and should expire based on time (say 10 minutes lifetime). Since, this is cross server communication, I cannot use session.

I am afraid I cannot use database or session to store/validate the token. Any code samples would be helpful.

This is required in PHP environment.


Solution

  • What you could do is add a timestamp to the token key as when it was created plus the requesters IP and then when you decrypt the key check if the time falls between your allowed time or allow ip address.

    example with fixed IP:

    <?php
    class csrf_check {
    
        const SALT = '_SECRET_';
    
        public function create_api_key()
        {
            return base64_encode($this->encrypt(time().'|'.$_SERVER['REMOTE_ADDR'])); // !change if you dont want IP check
        }
    
        public function check_api_key($key, $timeout = 5)
        {
            if (empty($key)) exit('Invalid Key');
    
            $keys = explode('|', $this->decrypt(base64_decode($key)));
    
            return (
                isset($key, $keys[0], $keys[1]) && 
                $keys[0] >= (time() - $timeout) && 
                $keys[1] == $_SERVER['REMOTE_ADDR'] // !change if you dont want IP check
            );
        }
    
        public function encrypt($string, $key = 'PrivateKey', $secret = 'SecretKey', $method = 'AES-256-CBC') {
            // hash
            $key = hash('sha256', $key);
            // create iv - encrypt method AES-256-CBC expects 16 bytes
            $iv = substr(hash('sha256', $secret), 0, 16);
            // encrypt
            $output = openssl_encrypt($string, $method, $key, 0, $iv);
            // encode
            return base64_encode($output);
        }
    
        public function decrypt($string, $key = 'PrivateKey', $secret = 'SecretKey', $method = 'AES-256-CBC') {
            // hash
            $key = hash('sha256', $key);
            // create iv - encrypt method AES-256-CBC expects 16 bytes
            $iv = substr(hash('sha256', $secret), 0, 16);
            // decode
            $string = base64_decode($string);
            // decrypt
            return openssl_decrypt($string, $method, $key, 0, $iv);
        }
    }
    
    $csrf = new csrf_check();
    
    //start example 
    
    $do = filter_input(INPUT_GET, 'do');
    $key = filter_input(INPUT_GET, 'key');
    
    switch ($do) {
        //example.com?do=get - a key for the request
        case "get": {
            $key = $csrf->create_api_key();
            echo '<a href="?do=check&key='.urlencode($key).'">Check Key ('.$key.')</a>';
        } break;
    
        //example.com?do=check - a key for the request
        case "check": {
            //key only lasts 30 secs & validate key passed
            //example.com?do=check&key=MEV6NXk4UjVRQXV5Qm1CMjBYa3RZZUhGd2M0YnFBUVF0ZkE5TFpNaElUTT0=
    
            echo 'Key ' . ($csrf->check_api_key($key, 30) ? 'valid' : 'invalid');
            echo '<br><a href="?do=get">Get new key</a>';
        } break;
    
        default: {
            echo '<a href="?do=get">Get Key</a>';
        } break;
    }