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
- Log in as a user with user manager role privileges.
- Navigate to the
Users
menu on the left navigation bar. - Open the details of the
User
you wish to generate anApiKey
for, and expand the > API keys section.

- Click on Generate new API key, which triggers a change request to create an
ApiKey
for thisUser
, which needs approval by an additional user with user manager role privileges. - Log in with a different user manager
- Navigate to the
Changes -> To Validate
menu and tab - Approve the change request

- You can now go back to the
User
details page where it will display anApiKey

- 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.

- 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.
- 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 aPOST
orPUT
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
- 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()
- The signature is then encoded in base64
signature = base64.b64encode(signatureBytes).decode('ascii')
- 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
- 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)
});
Updated 21 days ago