> This page is for Taurus PROTECT, version v3.40.
> For other versions, use one of these documentation indexes:
> - v3.58 (default): https://taurushq.ferndocs.com/protect-capital/v3.58/llms.txt
> - v3.56: https://taurushq.ferndocs.com/protect-capital/v3.56/llms.txt
> - v3.54: https://taurushq.ferndocs.com/protect-capital/v3.54/llms.txt
> - v3.52: https://taurushq.ferndocs.com/protect-capital/v3.52/llms.txt
> - v3.50: https://taurushq.ferndocs.com/protect-capital/v3.50/llms.txt
> - v3.48: https://taurushq.ferndocs.com/protect-capital/v3.48/llms.txt
> - v3.46: https://taurushq.ferndocs.com/protect-capital/v3.46/llms.txt
> - v3.44: https://taurushq.ferndocs.com/protect-capital/v3.44/llms.txt
> - v3.42: https://taurushq.ferndocs.com/protect-capital/v3.42/llms.txt
> - v3.40: https://taurushq.ferndocs.com/protect-capital/v3.40/llms.txt
> - v3.38: https://taurushq.ferndocs.com/protect-capital/v3.38/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://taurushq.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://taurushq.ferndocs.com/_mcp/server.

# Hmac Based Authentication

> **Note**
>
> **In this article**
>
> By the end of this document you should have:
>
> * 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.
> * Familiarity with basic code examples (in Python) which show you how to generate signatures and prepare auth headers.

## 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 request. 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 in protect 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:

1. Generate an API Key in the UI which is used to sign requests.
2. Compute an HMAC Signature and base64 encode it.
3. 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**.

1. Log in as a user with **user manager** role privileges.
2. Navigate to the `Users`menu on the left navigation bar and open the details for the user you would like to generate keys for.
3. Expand the `API keys` section and select the `HMAC`tab.

![](/_fern-img/c4de58c3f05abdd9c36e3173c56006b07b553f7147bb0829daae2e3c77e7bdf9.webp)

4. Click on **Generate new API key**, which triggers a change request to create an `ApiKey` for this `User`. This change request must be approved by an additional user with **user manager** role privileges (typically an administrator).
5. Log in with a different **user manager**.
6. Navigate to the `Changes -> To Validate` menu and tab.
7. Approve the change request.

![](/_fern-img/efa2ab5991a57290ec66820eb88b61742589348cec8fab10046d46a052e11a6b.webp)

8. You can now go back to the `User` details page where it will display an `ApiKey`.

![](/_fern-img/0b862ec7e918ca2edec879f0dad3cb386bb65ee7b329ff688cba5d7d22753059.webp)

9. Click on the eye icon next to the API key to **reveal** the secret.

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

![](/_fern-img/aafef6ce66b46eb0ce332ca1ac8c5cea2bfd0ee833b48355ab100c87ce8b3fc3.webp)

10. 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 be:

* Collecting 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.](/_fern-img/66d47a4877bc28351300d7256826f1eb52b2aeff64624e9f7d469f5260d13197.webp)\


|   | HMAC Part      | Description                                                                                                                                                                                                 |
| - | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | `prefix`       | Identifies the protocol version. Currently only`TPV1` 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`).                                                                                                                                             |
| 6 | `HTTP host`    | The network location where the request is being sent. FQDN + port number.  E.g., example.com:8080 (do **not** include the protocol (`https://` or `http://`) as this will result an invalid HMAC signature. |
| 7 | `path`         | The API request path (e.g., `/api/rest/v1/requests/approve`)                                                                                                                                                |
| 8 | `query`        | Any query parameters sent in the request  (e.g., `query=BTC`).                                                                                                                                              |
| 9 | `content-type` | Usually `application/json`.                                                                                                                                                                                 |
| 0 | `body`         | The body of the request. Usually a JSON string.                                                                                                                                                             |

The strings above are concatenated into a single string (in order) separated by spaces.

> **Warning**
>
> **Important**
>
> Any empty elements should be omitted as done in the Python example here:

**`Preparing the parts & body`**

```python Preparing the parts & body
parts = [
    "TPV1",
    self.key,
    nonce,
    timestamp,
    method,
    host,
    path,
    query,
    content_type
]
parts = [p for p in parts if p and p != ""] // omit empty parts
msg = bytes(' '.join(parts), 'utf-8') // separate by space
if body: 
    msg += bytes(' ','utf-8') // add a space and generate a byte sequence from the body
    msg += bytes(body, 'utf-8') 
```

2. The byte sequence is then signed using the `API Key` generated earlier.

**`Generate the HMAC signature`**

```python Generate the HMAC signature
signatureBytes = hmac.new(
  bytes.fromhex(self.secret),
  msg = msg,
  digestmod = hashlib.sha256
).digest()
```

3. The final step is to take the generated signature and to Base64 encode it.

**`Base 64 encode for use in header`**

```python Base 64 encode for use in header
signature = base64.b64encode(signatureBytes).decode('ascii')
```

4. 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 steps 1 - 3 above.

**`Sample script to generate the header (in Python)`**

```python Sample script to generate the header (in Python)
auth_header = 'TPV1-HMAC-SHA256 ApiKey={} Nonce={} Timestamp={} Signature={}'.format(self.key, nonce, timestamp, signature)
req.headers['Authorization'] = auth_header
```

5. The request can now be sent together with the `Authorization` header above.

## Proxy Server for HMAC requests

These code samples implement proxy servers in Python, Java, and C# to demonstrate the use of the HMAC signature scheme for a real application.

In other sections of this Taurus documentations, we assume you are able to run at least one of these samples to make sure authentication requirements are handled for other workflows.

**`Python`**

```python Python
"""
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() 
```

**`Java`**

```java Java
package com.taurussigner.hmac;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 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.
 */
@SpringBootApplication
public class HmacAuthProxy {

    private final String destination;
    private final String apiKey;
    private final byte[] apiSecret;

    public HmacAuthProxy(String destination, String apiKey, String apiSecret) {
        this.destination = destination.endsWith("/") ? destination.substring(0, destination.length() - 1) : destination;
        this.apiKey = apiKey;
        this.apiSecret = hexStringToByteArray(apiSecret);
    }

    private static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }

    public void start(int port) {
        System.setProperty("proxy.destination", destination);
        System.setProperty("proxy.apiKey", apiKey);
        System.setProperty("proxy.apiSecret", Base64.getEncoder().encodeToString(apiSecret));
        System.setProperty("server.port", String.valueOf(port));
        
        SpringApplication.run(HmacAuthProxy.class);
        System.out.println("Starting proxy on port " + port + " redirecting to " + destination + " ...");
    }

    @Component
    public static class ServerPortCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
        @Override
        public void customize(ConfigurableWebServerFactory factory) {
            factory.setPort(Integer.parseInt(System.getProperty("server.port", "9000")));
        }
    }

    @Component
    public static class ProxyFilter extends OncePerRequestFilter {

        private final String destination;
        private final String apiKey;
        private final byte[] apiSecret;

        public ProxyFilter() {
            this.destination = System.getProperty("proxy.destination");
            this.apiKey = System.getProperty("proxy.apiKey");
            this.apiSecret = Base64.getDecoder().decode(System.getProperty("proxy.apiSecret"));
        }

        @Override
        protected void doFilterInternal(
                HttpServletRequest request,
                HttpServletResponse response,
                FilterChain filterChain) throws ServletException, IOException {

            // Get request details
            String method = request.getMethod();
            String path = request.getRequestURI();
            String query = request.getQueryString() != null ? request.getQueryString() : "";
            String contentType = request.getContentType() != null ? request.getContentType() : "";

            // Read request body if present
            byte[] body = readRequestBody(request);

            // Create destination URL
            URL url = new URI(destination + path + (query.isEmpty() ? "" : "?" + query)).toURL();
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod(method);

            // Copy headers from original request
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                String headerValue = request.getHeader(headerName);
                if (!headerName.equalsIgnoreCase("host")) {
                    connection.setRequestProperty(headerName, headerValue);
                }
            }

            // Add Host header
            connection.setRequestProperty("Host", url.getHost());

            try {
                // Add authorization header
                String authHeader = generateAuthHeader(
                        method,
                        url.getHost(),
                        url.getPath(),
                        query,
                        contentType,
                        body
                );
                connection.setRequestProperty("Authorization", authHeader);

                // Set up for output if needed
                if (body.length > 0) {
                    connection.setDoOutput(true);
                    try (OutputStream os = connection.getOutputStream()) {
                        os.write(body);
                    }
                }

                // Get response
                int statusCode = connection.getResponseCode();
                response.setStatus(statusCode);

                // Copy response headers
                for (String headerName : connection.getHeaderFields().keySet()) {
                    if (headerName != null && 
                        !headerName.equalsIgnoreCase("Transfer-Encoding") && 
                        !headerName.equalsIgnoreCase("Content-Length")) {
                        String headerValue = connection.getHeaderField(headerName);
                        response.setHeader(headerName, headerValue);
                    }
                }

                // Copy response body
                try (InputStream is = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream()) {
                    if (is != null) {
                        try (OutputStream os = response.getOutputStream()) {
                            byte[] buffer = new byte[4096];
                            int bytesRead;
                            while ((bytesRead = is.read(buffer)) != -1) {
                                os.write(buffer, 0, bytesRead);
                            }
                        }
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                response.getWriter().write("Error proxying request: " + e.getMessage());
            }
        }

        private byte[] readRequestBody(HttpServletRequest request) throws IOException {
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                if (request.getContentLength() > 0) {
                    try (InputStream is = request.getInputStream()) {
                        byte[] buffer = new byte[4096];
                        int bytesRead;
                        while ((bytesRead = is.read(buffer)) != -1) {
                            baos.write(buffer, 0, bytesRead);
                        }
                    }
                }
                return baos.toByteArray();
            }
        }

        private String generateAuthHeader(String method, String host, String path, String query, String contentType, byte[] body) 
                throws NoSuchAlgorithmException, InvalidKeyException, IOException {
            
            String nonce = UUID.randomUUID().toString();
            String timestamp = String.valueOf(Instant.now().toEpochMilli());

            List<String> parts = new ArrayList<>();
            parts.add("TPV1");
            parts.add(apiKey);
            parts.add(nonce);
            parts.add(timestamp);
            parts.add(method);
            parts.add(host);
            parts.add(path);
            parts.add(query);
            parts.add(contentType);

            // Remove any empty parts
            parts.removeIf(String::isEmpty);
            String message = String.join(" ", parts);
            
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            outputStream.write(message.getBytes(StandardCharsets.UTF_8));
            
            // If there's a body, append it to the message
            if (body.length > 0) {
                outputStream.write(' ');
                outputStream.write(body);
            }
            
            byte[] messageBytes = outputStream.toByteArray();

            // Create HMAC-SHA256 signature
            Mac hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(apiSecret, "HmacSHA256");
            hmac.init(keySpec);
            byte[] signatureBytes = hmac.doFinal(messageBytes);
            String signature = Base64.getEncoder().encodeToString(signatureBytes);

            return String.format("TPV1-HMAC-SHA256 ApiKey=%s Nonce=%s Timestamp=%s Signature=%s", apiKey, nonce, timestamp, signature);
        }
    }
    
    public static void main(String[] args) {
        if (args.length < 6) {
            System.out.println("Usage: java -jar hmac-auth-proxy.jar --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) {
                        try {
                            port = Integer.parseInt(args[++i]);
                        } catch (NumberFormatException e) {
                            System.err.println("Error: port must be a number");
                            return;
                        }
                    }
                    break;
                case "--destination":
                case "-d":
                    if (i + 1 < args.length) {
                        destination = args[++i];
                    }
                    break;
                case "--key":
                case "-k":
                    if (i + 1 < args.length) {
                        apiKey = args[++i];
                    }
                    break;
                case "--secret":
                case "-s":
                    if (i + 1 < args.length) {
                        apiSecret = args[++i];
                    }
                    break;
            }
        }

        if (destination == null || apiKey == null || apiSecret == null) {
            System.err.println("Error: destination, key, and secret are required arguments");
            return;
        }

        HmacAuthProxy proxy = new HmacAuthProxy(destination, apiKey, apiSecret);
        proxy.start(port);
    }
} 
```

**`C#`**

```csharp C#
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

A `Pre-request Script` that can be used in the [`Postman` graphical API workbench tool](https://www.postman.com). 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, so please make sure to keep your client up-to-date.

**`Postman Pre-request script`**

```javascript Postman Pre-request script
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)
});
```