In general, an API signature consists of 4 ingredients:

  1. The data (path, method, query and/or payload)
  2. A secret salt (pre-shared secret, API key, token, etc)
  3. The timestamp
  4. The version of the signature

(1) and (2) are usually mandatory, (3) is needed when you want the signature to be time sensitive and harder to reproduce again at a later time without knowing the signature hashing algorithm.

(4) is rarely used. It is only needed when you plan to have multiple versions of the hashing algorithm.

Generating the signature hash

(1) First, concatenate the necessary data into one data string. We use a dot separator, but this is optional. Example:

<Secret>.<Epoch Timestamp>.<HTTP method>.<URL Path>.<Query String>.<Payload>

27e6cfc6d6435c4b626c3022b93f8cf37b6.1497164708.post./reports/1.apikey=123456.{"name":"report 1"}

(2) For consistency and reproducibility, we usually set some rules.

  • Everything in the data string should be in one case - uppercase or lowercase.
  • The order of the query strings. E.g. alphabetically / dictionary order.

(3) Next, we can create an SHA256 hash using the above data string.

Digest::SHA256.hexdigest('27e6cfc6d6435c4b626c3022b93f8cf37b6.1497164708.post./reports/1.apikey=123456.{"name":"report 1"}'.downcase)
# 2188462a1206ab317ad9518098aef588036311025d8bab97385c3e05766fbc08

(4) Some information is necessary to be sent as plain text so the remote party can reproduce the signature. This usually includes the timestamp, the key of the pre-shared secret and/or the version. We can send the pre-shared key as a separate custom header and the full signature string will have the following format:

<version>:<epoch timestamp>:<data string hash>


Reproduce and validate

To reproduce the signature hash, we have to first reproduce the data string.

(1) We get the signature from the request header. E.g.

signature = request.headers['HTTP_X_MY_SIGNATURE']

(2) Extract the 3 sections - version, epoch timestamp and hashed data string.

signature = xv2.split(':')
signature[0] # The Version
signature[1] # The Epoch Timestamp
signature[2] # The Data String Hash

(3) Get the HTTP method of the request.

method = request.method

(4) Get the path URL.

path = request.path

(5) Get the query string. We need to sort the queries by alphabet order.

r = Hash[request.query_parameters.sort]
query_string = ''
r.each {|k,v| query_string << "#{k}=#{v}&" }
query_string = query_string[0...-1]

(6) Get the payload.

payload = request.raw_post

(7) Calculate the data string hash from above information.

calculated_hash = Digest::SHA256.hexdigest("#{secret}.#{signature[1]}.#{method}.#{path}.#{query_string}.#{payload}").downcase

(8) Finally, you compare the calculated hash and the hash supplied by the request. If they match, you are good to go.

calculated_hash == signature[2]

Additional Checking

(1) You should also check the epoch timestamp is within reasonable time comparing to your server time. E.g. plus or minus 5 minutes.

current_timestamp = Time.now.getutc.to_i
timestamp = signature[1].to_i

if (current_timestamp - timestamp).abs > 300
  return false

(2) The hash algorithm version is a supported version. Not obsoleted and not an unknown version.