Hmac Based Authentication
Available starting Taurus-PROTECT 3.20
This article covers
- A high level understanding of what HMAC authentication is.
- When to use HMAC authentication and how it differs from the Bearer authentication.
- Familiarity with how Taurus implements HMAC including which fields we sign and how to generate the headers.
- How to use the Java example SDK to create an API client that encapsulates HMAC authentication
- A proxy server as additional examples in Python and C#
- An HMAC authentication script for the popular Postman API client.
Introduction
HMAC (Hash-based Message Authentication Code) is a method for verifying the integrity and authenticity of a message using a shared secret key. It combines the message and key with a cryptographic hash function (like SHA-256) to produce a signature. This signature ensures the request hasnβt been altered and comes from a trusted source β making it a common choice for securing API requests.
This approach differs from Bearer authentication in that it allows long-lived keys to be used. While Bearer authentication uses the username and password directly to be exchanged for a short-lived token, using the HMAC authentication method allows multiple pre-shared keys which can be secured independently in a secret store, eg. Vault or Azure Key Vault to sign requests. Since multiple keys can be stored for each user, 0-downtime key rotation is possible even with eventually consistent key storage systems. Every request must be sent with a signature in the Authorization
header.
When to use HMAC Authentication
While the Bearer Authentication describes an approach which works well for development purposes, we recommend implementing production systems against the more robust scheme described in this document. Since keys can be independently secured and rotated, using the HMAC scheme provides tangible security benefits for operating a Taurus-PROTECT installation in business critical environments.
High Level Approach
This document covers, with working code examples and setup instructions, the following high-level flow:
- Generate an API Key in the UI which is used to sign requests.
- Compute an HMAC Signature and base64 encode it.
- Create an API request via with the signature in the
Authorization
header.
Obtaining an API Key via the UI
Adding a new API key to a user using the Taurus UI adheres to the 4-eyes principle and will require participation of at least two users with user manager roles, typically your administrators.
- Log in as a user with user manager role privileges.
- Navigate to the
Users
menu on the left navigation bar and open the details for the user you would like to generate keys for. - Expand the
API keys
section and select theHMAC
tab.

- Click on Generate new API key, which triggers a change request to create an
ApiKey
for thisUser
. This change request must be approved by an additional user with user manager role privileges (typically an administrator). - 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.
Important
You will only have one opportunity to record the API key. Ensure you save this in a safe place. If the secret is lost, all the above steps must be repeated to generate a new key and the lost key should be deleted.

- Both the Id and the Secret, are required for signing a request. You don't need to treat the Id field as sensitive data, but the Secret value must be protected. Make sure you store these in a safe place.
Signing requests using the HMAC secret
This section walks through the steps involved in generating and verifying HMAC signatures β including how to construct the message, handle timestamps and nonces, and securely transmit the signature with each request. Following these conventions ensures both sides can trust the integrity and origin of the data.
At a high level, to generate an HMAC signature you will:
- Collect 10 pieces of information (called HMAC parts).
- Concatenate the parts with one whitespace character (
0x20
) in the correct order. Skip empty parts (ie. do not use double spaces). - Generate a
HMAC-SHA265
signature with the API key and the combined HMAC parts.
The HMAC Signature is made up of 9 parts plus the request body and the table below describes each part in detail. Note that parts 2,3,4 will be present in both the signature and the header. This is important for preventing replay attacks.

See the table below for more details about each part of the HMAC.
. | HMAC Part | Description |
---|---|---|
1 | prefix | Identifies the protocol version. Currently onlyTPV1 is supported. |
2 | apiKey | The user's API Key generated in the previous section |
3 | nonce | A string that needs to be different with every request. Usually a UUID or a number |
4 | timestamp | Milliseconds since the Unix Epoch. |
5 | HTTP method | The HTTP method used with this request (e.g., POST or GET ). All upper case. |
6 | HTTP host | The fully qualified hostname (FQDN) the request is being sent to. (e.g.,tg-validatord-instance.t-dx.com ). |
7 | path | The API request path. |
8 | query | Any query parameters sent in the request (e.g. query=BTC ). |
9 | content-type | Usually application/json . |
10 | body | The body of the request. Usually a JSON string. |
When using the Java SDK, HMAC authentication is performed through the ProtectClient class and its internal ApiClient object. Setting up API calls via HMAC authentication is as simple as follows:
import com.taurushq.sdk.protect.client.ProtectClient;
import com.taurushq.sdk.protect.openapi.auth.ApiKeyTPV1Exception;
class ProtectClientExample {
// URL to the Protect API host
private static final String host = "http://localhost:6000";
// The API key, usually a uuid string
private static final String apiKey = "862d497f-a96b-4191-a285-d3f0a09b8946";
// The API secret, usually a string of hexadecimal characters without the leading '0x'
// This string should be kept secret and retrieved from a secured location
private static final String apiSecret = "deadbeef";
public static void main(String[] args) throws ApiKeyTPV1Exception {
ProtectClient client = new ProtectClient(host, apiKey, apiSecret);
// Reveal supported authentication methods - currently just ApiKeyTPV1
System.out.println(client.getOpenApiClient().getAuthentications());
}
}
Here's sample code taken from the Java SDK that shows what is does internally to prepare the HTTP Authentication
header:
- The to be signed parts are concatenated into a single string (in order matching the table above) separated by spaces.
- Empty parameters are removed such that the resulting concatenated string does not have two consecutive spaces.
- The string from step 2 above is then hashed and signed with a standard Message Authentication Code.
- Finally the Authorization header is created using the following format:
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 used in the HMAC signature from step 3 and that same signature is also the value used for Signature.
import com.google.common.base.Strings;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.HmacUtils;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.commons.codec.digest.HmacAlgorithms.HMAC_SHA_256;
class CryptoTPV1 {
public static String calculateBase64Hmac(byte[] secret, String data) {
// Use the HMAC_SHA_256 Message Authentication Code (MAC) algorithm
// specified in RFC 2104 and FIPS PUB 180-2.
return Base64.encodeBase64String(new HmacUtils(HMAC_SHA_256, secret).hmac(data));
}
public static String calculateSignedHeader(
String apiKey, byte[] apiSecret, String nonce, long timestamp,
String method, String host, String path, String query,
String contentType, String body) {
// 1. Concatenate the strings to be signed in the exact order shown
// 2. Remove null or empty strings
// 3. String components are joined with a single space character as separator
String msg = Stream.of("TPV1", apiKey, nonce,
Long.toString(timestamp), method, host,
path, query, contentType, body)
.filter(p -> !Strings.isNullOrEmpty(p))
.collect(Collectors.joining(" "));
// Create Authentication header string
return String.format("TPV1-HMAC-SHA256 ApiKey=%s Nonce=%s Timestamp=%s Signature=%s",
apiKey, nonce, timestamp, calculateBase64Hmac(apiSecret, msg));
}
public static void main(String[] args) {
System.out.println(CryptoTPV1.calculateSignedHeader("api-key", "api-secret".getBytes(),
"nonce", 10, "POST",
"https://acme.com", "api/path",
"query", "application/json",
"{}"));
}
}
The code above prints out the generated string to be used in an HTTPAuthorization
header.
An API call done with the Java SDK ProtectClient
will do all of the above for you.
Proxy Server for HMAC requests
For testing and to provide additional example code in additional languages, you can use the following http server proxy. It forwards incoming requests to the command line provided Protect API server and adds the Authentication header using the command line provided API key and secret.
"""
HMAC Authentication Proxy Server
This proxy server listens on a specified local port and forwards all requests
to the configured URL, while adding an authorization header based on the
supplied API key.
Usage example:
python hmac_auth_proxy.py --port 9000 --destination https://example.com --key your-api-key --secret your-api-secret
This implementation uses Flask and requests for handling HTTP operations more robustly.
requirements.txt:
flask==2.3.3
cryptography==41.0.4
Werkzeug==2.3.7
requests==2.31.0
"""
import argparse
import base64
import hashlib
import hmac
import time
import uuid
from urllib.parse import urlparse, urlunparse
import requests
from flask import Flask, request, Response, stream_with_context
class HmacAuthProxy:
def __init__(self, destination, api_key, api_secret):
self.destination = destination.rstrip('/')
self.api_key = api_key
self.api_secret = bytes.fromhex(api_secret)
self.app = Flask(__name__)
self._setup_routes()
def _setup_routes(self):
@self.app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@self.app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
def proxy(path):
url_parts = urlparse(f"{self.destination}/{path}")
method = request.method
query = request.query_string.decode('utf-8') if request.query_string else ""
content_type = request.headers.get('Content-Type', '')
body = request.get_data() if request.data else None
# Create a new headers dictionary from the original request
headers = dict(request.headers)
# Update host in headers
headers['Host'] = url_parts.netloc
# Generate and set authorization header
headers['Authorization'] = self._generate_auth_header(
method,
url_parts.netloc,
url_parts.path,
query,
content_type,
body
)
headers.pop('Content-Length', None)
headers.pop('Content-Encoding', None)
headers.pop('Transfer-Encoding', None)
# Build the target URL
target_url = urlunparse((
url_parts.scheme,
url_parts.netloc,
url_parts.path,
url_parts.params,
query,
url_parts.fragment
))
# Make the request to the target URL
resp = requests.request(
method=method,
url=target_url,
headers=headers,
data=body,
stream=True
)
# Create a Flask response object from the requests response
response = Response(
stream_with_context(resp.iter_content(chunk_size=1024)),
status=resp.status_code,
content_type=resp.headers.get('Content-Type')
)
# Copy response headers to the Flask response
for key, value in resp.headers.items():
if key.lower() not in ('content-length', 'content-encoding', 'transfer-encoding'):
response.headers[key] = value
return response
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.api_key,
nonce,
timestamp,
method,
host,
path,
query,
content_type
]
# Remove any empty parts
parts = [p for p in parts if p and p != ""]
# Join the parts with spaces
message = ' '.join(parts).encode('utf-8')
# If there's a body, append it to the message
if body:
message = message + b' ' + body
# Create HMAC-SHA256 signature
signature_bytes = hmac.new(self.api_secret, msg=message, digestmod=hashlib.sha256).digest()
signature = base64.b64encode(signature_bytes).decode('ascii')
return f'TPV1-HMAC-SHA256 ApiKey={self.api_key} Nonce={nonce} Timestamp={timestamp} Signature={signature}'
def run(self, port=9000):
print(f"Starting proxy on port {port} redirecting to {self.destination} ...")
self.app.run(host='0.0.0.0', port=port)
def main():
# Parse command line arguments
parser = argparse.ArgumentParser(description='HMAC Authentication Proxy')
parser.add_argument('-p', '--port', type=int, default=9000, help='Local port to listen on')
parser.add_argument('-d', '--destination', required=True, help='Destination host to forward to')
parser.add_argument('-k', '--key', required=True, help='API key ID')
parser.add_argument('-s', '--secret', required=True, help='API key secret (hex-encoded)')
args = parser.parse_args()
# Create and run the proxy
proxy = HmacAuthProxy(args.destination, args.key, args.secret)
proxy.run(args.port)
if __name__ == '__main__':
main()
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace TaurusSigner.HmacAuth
{
/// <summary>
/// HMAC Authentication Proxy Server
///
/// This proxy server listens on a specified local port and forwards all requests
/// to the configured URL, while adding an authorization header based on the
/// supplied API key.
/// </summary>
public class HmacAuthProxy
{
private readonly string _destination;
private readonly string _apiKey;
private readonly string _apiSecret;
private readonly HttpClient _httpClient;
public HmacAuthProxy(string destination, string apiKey, string apiSecret)
{
_destination = destination.TrimEnd('/');
_apiKey = apiKey;
_apiSecret = apiSecret;
_httpClient = new HttpClient();
}
public void Start(int port)
{
var builder = WebApplication.CreateBuilder();
var app = builder.Build();
app.UseMiddleware<ProxyMiddleware>(this);
Console.WriteLine($"Starting proxy on port {port} redirecting to {_destination}...");
app.Run($"http://localhost:{port}");
}
private string GenerateAuthHeader(string method, string host, string path, string query, string contentType, byte[] body)
{
var nonce = Guid.NewGuid().ToString();
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
var parts = new List<string>
{
"TPV1",
_apiKey,
nonce,
timestamp,
method,
host,
path,
query,
contentType
};
// Remove any empty parts
parts.RemoveAll(string.IsNullOrEmpty);
var message = string.Join(" ", parts);
using var hmac = new HMACSHA256(Convert.FromHexString(_apiSecret));
var messageBytes = Encoding.UTF8.GetBytes(message);
// If there's a body, append it to the message
if (body != null && body.Length > 0)
{
using var ms = new MemoryStream();
ms.Write(messageBytes, 0, messageBytes.Length);
ms.Write(new byte[] { (byte)' ' }, 0, 1);
ms.Write(body, 0, body.Length);
messageBytes = ms.ToArray();
}
var signatureBytes = hmac.ComputeHash(messageBytes);
var signature = Convert.ToBase64String(signatureBytes);
return $"TPV1-HMAC-SHA256 ApiKey={_apiKey} Nonce={nonce} Timestamp={timestamp} Signature={signature}";
}
private class ProxyMiddleware
{
private readonly RequestDelegate _next;
private readonly HmacAuthProxy _proxy;
public ProxyMiddleware(RequestDelegate next, HmacAuthProxy proxy)
{
_next = next;
_proxy = proxy;
}
public async Task InvokeAsync(HttpContext context)
{
var method = context.Request.Method;
var path = context.Request.Path.ToString();
var query = context.Request.QueryString.ToString();
var contentType = context.Request.ContentType ?? string.Empty;
// Read the request body
byte[] body = null;
if (context.Request.ContentLength > 0)
{
using var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms);
body = ms.ToArray();
context.Request.Body = new MemoryStream(body);
}
// Create a new HttpRequestMessage for the destination
var requestUri = new Uri($"{_proxy._destination}{path}{query}");
var request = new HttpRequestMessage(new HttpMethod(method), requestUri);
// Copy headers from original request
foreach (var header in context.Request.Headers)
{
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && request.Content != null)
{
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
// Add the authorization header
var authHeader = _proxy.GenerateAuthHeader(
method,
requestUri.Host,
requestUri.AbsolutePath,
query.TrimStart('?'),
contentType,
body
);
request.Headers.TryAddWithoutValidation("Authorization", authHeader);
// Add content if needed
if (body != null && body.Length > 0)
{
request.Content = new ByteArrayContent(body);
if (!string.IsNullOrEmpty(contentType))
{
request.Content.Headers.Remove("Content-Type");
request.Content.Headers.TryAddWithoutValidation("Content-Type", contentType);
}
}
// Send the request to the destination
var response = await _proxy._httpClient.SendAsync(request);
// Copy status code
context.Response.StatusCode = (int)response.StatusCode;
// Copy response headers
foreach (var header in response.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
if (response.Content != null)
{
foreach (var header in response.Content.Headers)
{
if (!header.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
}
// Copy response body
await response.Content.CopyToAsync(context.Response.Body);
}
}
}
}
public class Program
{
public static void Main(string[] args)
{
if (args.Length < 6)
{
Console.WriteLine("Usage: dotnet run --port <port> --destination <destination> --key <apiKey> --secret <apiSecret>");
return;
}
int port = 9000;
string destination = null;
string apiKey = null;
string apiSecret = null;
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--port":
case "-p":
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int p))
{
port = p;
i++;
}
break;
case "--destination":
case "-d":
if (i + 1 < args.Length)
{
destination = args[i + 1];
i++;
}
break;
case "--key":
case "-k":
if (i + 1 < args.Length)
{
apiKey = args[i + 1];
i++;
}
break;
case "--secret":
case "-s":
if (i + 1 < args.Length)
{
apiSecret = args[i + 1];
i++;
}
break;
}
}
if (string.IsNullOrEmpty(destination) || string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(apiSecret))
{
Console.WriteLine("Error: destination, key, and secret are required arguments");
return;
}
var proxy = new HmacAuthProxy(destination, apiKey, apiSecret);
proxy.Start(port);
}
}
}
Postman Pre-request script
In this section we provide aPre-request Script
that can be used in the Postman
graphical API workbench tool. 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. This requires a recent version of Postman, so please make sure to keep your client up-to-date.
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 8 days ago