Hmac Based Authentication

Available starting Taurus-PROTECT 3.20

This method provides integrity in addition to authenticity checks, as each request is signed with a secret key. The key does not expire and is valid until revoked.
As the secret key is exclusively used for API authentication it is calledApiKey.

An ApiKey can be generated for any Taurus-PROTECT or Taurus-CAPITAL user, and can be obtained via the Graphical User Interface as described below.

This shared secret is then used to sign every API request.

Obtaining an ApiKey via the UI

  1. Log in as a user with user manager role privileges.
  2. Navigate to the Usersmenu on the left navigation bar.
  3. Open the details of the User you wish to generate an ApiKey for, and expand the > API keys section.
  1. Click on Generate new API key, which triggers a change request to create an ApiKey for this User, which needs approval by an additional user with user manager role privileges.
  2. Log in with a different user manager
  3. Navigate to the Changes -> To Validate menu and tab
  4. Approve the change request
  1. You can now go back to the User details page where it will display an ApiKey
  1. Click on the eye icon next to the API key to reveal the secret

🚧

Careful

Revealing a secret can be done only once. If the secret is lost, a new ApiKey needs to be created and the ApiKey corresponding to the lost secret should be revoked.

  1. Both strings, the Id and the Secret, are required for successful HMAC authentication. Make sure you store these in a safe place.

Using the HMAC-authentication method

The HMAC-authentication is based on cryptographic signatures. Instead of providing a JWT that represents a “session” when doing a request, the User signs every API request.

  1. To generate the signature of a request the following information is required for the authentication header:
  • a prefix, identifying the protocol version. Currently only TPV1 is supported.
  • the User’s ApiKey identifier (The unique Id - see section above)
  • a nonce, a string that needs to be different with every request (usually a UUID)
  • a timestamp, as a unix epoch timestamp in milliseconds
  • the HTTP method of the request (e.g. POST)
  • the HTTP host - the fully qualified host name the request is being sent to (e.g. tg-validatord-instance.t-dx.com)
  • the path of the request (e.g. /api/rest/v1/blockchains)
  • the query string of the request (e.g. ?query=BTC)
  • the content type of the request (usually application/json)
  • the body of the request as a string (empty for GET requests, usually JSON for a POST or PUT request e.g. {"query":"BTC"})

The strings above are concatenated into a single string, in the order listed. The individual string components are separated by spaces. Empty elements are not included.

parts = [
    "TPV1",
    self.key,
    nonce,
    timestamp,
    method,
    host,
    path,
    query,
    content_type
]
parts = [p for p in parts if p and p != ""]
msg = bytes(' '.join(parts), 'utf-8')
if body:
    msg += bytes(' ','utf-8')
    msg += body
  1. This is then signed with Hash-based message authentication code (HMAC), using the ApiKey secret for encryption.
signatureBytes = hmac.new(bytes.fromhex(self.secret), msg = msg, digestmod = hashlib.sha256).digest()
  1. The signature is then encoded in base64
signature = base64.b64encode(signatureBytes).decode('ascii')
  1. The Authorization header has the following form:

TPV1-HMAC-SHA256 ApiKey={} Nonce={} Timestamp={} Signature={}

Where TPV1-HMAC-SHA256 is a fixed string identifying the protocol. ApiKey, Nonce and Timestamp have to match the values that were signed in 1. - 3. above.

auth_header = 'TPV1-HMAC-SHA256 ApiKey={} Nonce={} Timestamp={} Signature={}'.format(self.key, nonce, timestamp, signature)
req.headers['Authorization'] = auth_header
  1. The request can now be sent together with the Authorization header above.

Examples

Here we provide 2 examples of this method.

The first example is a python script. This script can be started to launch a small proxy server that can receive a request, sign it on the fly, and forward it to the intended server. It demonstrates the signature mechanism based on the request’s content and the User’s ApiKey details

# This script demos an authentication proxy server.
# The proxy server listens on a specified local port and forwards all requests
# to the configured URL, whilst adding an authorization header based on the
# supplied API key.
#
# Usage example (illustrative):
#
# > python3 auth_proxy.py --port 9000 --destination https://tg-validatord-cockroach.t-dx.com --key f9553d35-83ef-4796-a3ea-eeb5558462df --secret bb38...a19f
#
# This software is provided for demonstration purposes only. As documented by the
# Python3 documentation ( https://docs.python.org/3/library/http.server.html )
# the HTTPServer class is not recommended for use in production systems.
# Production systems should provide authorization as part of request creation,
# to ensure the authenticity of signed requests.

import base64
import hashlib
import hmac
import requests
import time
import uuid
from functools import partial
from http.server import BaseHTTPRequestHandler,HTTPServer
from socketserver import ThreadingMixIn
from urllib.parse import urlparse, urlunparse


class Server(BaseHTTPRequestHandler):
    def __init__(self, destination, key, secret, *args, **kwargs):
        self.destination = destination
        self.key = key
        self.secret = secret
        super().__init__(*args, **kwargs)

    def do_GET(self):
        self._handle_request("GET", requests.get)

    def do_DELETE(self):
        self._handle_request("DELETE", requests.delete)

    def do_POST(self):
        self._handle_request("POST", requests.post)

    def do_PUT(self):
        self._handle_request("PUT", requests.put)

    def do_PATCH(self):
        self._handle_request("PATCH", requests.patch)

    def _handle_request(self, method, requests_func):
        url = urlparse(self.destination + self.path)
        content_type = self.headers["content-type"]
        body = self.rfile.read(int(self.headers["content-length"])) if self.headers["content-length"] else None

        headers = dict(self.headers)
        headers['Host'] = url.netloc
        headers['Authorization'] = self._generate_auth_header(method, url.netloc, url.path, url.query, content_type, body)

        resp = requests_func(urlunparse(url), data=body, headers=headers)

        self.send_response(resp.status_code)
        for key in resp.headers:
            # requests package decodes response, this changes content length and encoding
            if key == 'Content-Length':
                self.send_header('Content-Length', "{}".format(len(resp.content)))
            elif key == 'Content-Encoding':
                pass
            elif key == 'Transfer-Encoding':
                pass
            else:
                self.send_header(key, resp.headers[key])
        self.end_headers()
        self.wfile.write(resp.content)
        resp.close()

    def _generate_auth_header(self, method, host, path, query, content_type, body):
        nonce = str(uuid.uuid4())
        timestamp = str(int(time.time() * 1000))
        parts = [
            "TPV1",
            self.key,
            nonce,
            timestamp,
            method,
            host,
            path,
            query,
            content_type
        ]
        parts = [p for p in parts if p and p != ""]
        msg = bytes(' '.join(parts), 'utf-8')
        if body:
            msg += bytes(' ','utf-8')
            msg += body

        signatureBytes = hmac.new(bytes.fromhex(self.secret), msg = msg, digestmod = hashlib.sha256).digest()
        signature = base64.b64encode(signatureBytes).decode('ascii')

        header = 'TPV1-HMAC-SHA256 ApiKey={} Nonce={} Timestamp={} Signature={}'.format(self.key, nonce, timestamp, signature)
        return header


def run(port, destination, key, secret):
    class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
        """Handle requests in a separate thread."""

    httpd = ThreadedHTTPServer(('', port), partial(Server, destination, key, secret))

    print('Starting proxy on port {} redirecting to {} ...'.format(port, destination))
    httpd.serve_forever()

if __name__ == "__main__":
    import argparse

    argParser = argparse.ArgumentParser()
    argParser.add_argument("-p", "--port",        nargs='?', const=9000, type=int, help="local port to listen on")
    argParser.add_argument("-d", "--destination", nargs=1, required=True, help="destination host to forward to")
    argParser.add_argument("-k", "--key",         nargs=1, required=True, help="API key ID (uuid)")
    argParser.add_argument("-s", "--secret",      nargs=1, required=True, help="API key secret (hex-encoded)")
    args = argParser.parse_args()

    destination = args.destination[0]
    key = args.key[0]
    secret = args.secret[0]

    run(args.port, destination, key, secret)

The second example is a Pre-request Script that can be used in the Postman tool (Graphical UI tool to call APIs). Place the following script in the Pre-request Script section of your Postman Taurus-PROTECT setup, and make sure to store a valid apiKey and apiSecret in the Postman Vault (requires a recent version of Postman).

var url = require('url');
var uuid = require('uuid');
var CryptoJS = require('crypto-js')
var { Property } = require('postman-collection');

var apiKey = await pm.vault.get("apiKey");
var apiSecret = await pm.vault.get("apiSecret");

const authorizationScheme = 'TPV1-HMAC-SHA256';

function computeHmac(message, key_hex) {
    enc = CryptoJS.HmacSHA256(message, CryptoJS.enc.Hex.parse(key_hex));
    return CryptoJS.enc.Base64.stringify(enc);
}

function newNonce() {
    return uuid.v4();
}

function calculateAuthHeader(req) {
    let urlExpanded = Property.replaceSubstitutions(req.url.toString(), pm.variables.toObject());
    let parsedUrl = url.parse(urlExpanded);
    let hostname = parsedUrl.hostname;
    let port = parsedUrl.port;
    if ((port !== "") && (port !== null) && (port !== undefined)) {
        hostname = hostname + ":" + port;
    }
    let path = parsedUrl.pathname;
    let queryString = parsedUrl.query;
    let method = req.method;
    let dateStamp = Date.now().toString();
    let nonce = newNonce();
    let body = req.body.toString();
    let contentType = ''
    if (method === "POST" || method === "PUT") {
        contentType = 'application/json';
    }

    items = [
        "TPV1",
        apiKey,
        nonce,
        dateStamp,
        method,
        hostname,
        path,
        queryString,
        contentType,
        body,
    ];
    
    let payload = "";
    for (item of items) {
         if ((item !== "") && (item !== undefined) && (item !== null)) {
            if (payload.length > 0) {
                payload = payload + " ";    
            }
            payload = payload + item;
        }
    }

    sig = computeHmac(payload, apiSecret);
    return `${authorizationScheme} ApiKey=${apiKey} Nonce=${nonce} Timestamp=${dateStamp} Signature=${sig}`;
}

pm.request.headers.add({
    key: 'Authorization',
    value: calculateAuthHeader(pm.request)
});



  © 2025 Taurus SA. All rights reserved.