03_URL-Signatures

title: URL Signatures slug: url-signatures order: 3

✒️ How fetchdocs signs URLs

fetchdocs signs redirect URLs to prevent tampering by adding a signature to the URL. The signature is a hash of the URL, including the query parameters and a secret. The secret is your API key because it's the secret that you and fetchdocs share and that allows you to verify the signature.

For the hashing fetchdocs uses HMAC with SHA-256. This is a widely used and secure way to hash data. The hash is then encoded as a hexadecimal string and added to the URL as a query parameter.

🕵️‍♂️ How to verify the signature

To verify the signature, you need to calculate the hash of the URL and the secret and compare it to the signature in the URL. Here is an example of how to do this in some common languages:

PHP
$signedUrl = 'https://your-website.url/fetchdocs/callback?success=true&connection_uuid=123456789&signature=f8ebd16c088f8f7...'; $myApiKey = 'your-api-key'; // Replace with your actual API key, better fetch from your secret store. // First, we parse the URL to get the host, path and query string $host = parse_url($signedUrl, PHP_URL_HOST); $path = parse_url($signedUrl, PHP_URL_PATH); $queryString = parse_url($signedUrl, PHP_URL_QUERY); // Then, we parse the query string to get the query parameters as an array parse_str($queryString, $query); // We write the signature into an own variable $signature = $query['signature']; // We remove the signature from the query parameters because must not be part of the hash unset($query['signature']); // We sort the query parameters alphabetically by key so that the hash is always the same ksort($query); // We build the hash payload by writing the host, path and query parameters into an array $hashData = json_encode([ 'host' => $host, 'path' => $path ?? '/', // If there is no path, we use a slash 'query' => http_build_query($query), // We build the query parameters back into a query string ]) // Next, we calculate the hash of the hash payload and the secret $calculatedSignature = hash_hmac('sha256', $hashData, $myApiKey); // Finally, we compare the calculated signature with the signature from the URL hash_equals($calculatedSignature, $signature); // Expected to be true
PYTHON
import hashlib import json from urllib.parse import urlparse, parse_qs, urlencode signed_url = 'https://your-website.url/fetchdocs/callback?success=true&connection_uuid=123456789&signature=f8ebd16c088f8f7...' my_api_key = 'your-api-key' # Replace with your actual API key, better fetch from your secret store. # First, we parse the URL to get the host, path and query string parsed_url = urlparse(signed_url) host = parsed_url.hostname path = parsed_url.path query_string = parsed_url.query # Then, we parse the query string to get the query parameters as a dictionary query = parse_qs(query_string) # We write the signature into an own variable signature = query['signature'][0] # We remove the signature from the query parameters because it must not be part of the hash del query['signature'] # We sort the query parameters alphabetically by key so that the hash is always the same sorted_query = dict(sorted(query.items())) # We build the hash payload by writing the host, path and query parameters into a dictionary hash_data = json.dumps({ 'host': host, 'path': path if path else '/', # If there is no path, we use a slash 'query': urlencode(sorted_query) # We build the query parameters back into a query string }) # Next, we calculate the hash of the hash payload and the secret calculated_signature = hashlib.sha256(hash_data.encode('utf-8')).hexdigest() # Finally, we compare the calculated signature with the signature from the URL print(calculated_signature == signature) # Expected to be True
JAVASCRIPT
const url = 'https://your-website.url/fetchdocs/callback?success=true&connection_uuid=123456789&signature=f8ebd16c088f8f7...'; const myApiKey = 'your-api-key'; // Replace with your actual API key, better fetch from your secret store. // First, we parse the URL to get the host, path and query string const urlObject = new URL(url); const host = urlObject.hostname; const path = urlObject.pathname; const queryString = urlObject.searchParams.toString(); // Then, we parse the query string to get the query parameters as an object const queryParams = Object.fromEntries(new URLSearchParams(queryString)); // We write the signature into an own variable const signature = queryParams['signature']; // We remove the signature from the query parameters because it must not be part of the hash delete queryParams['signature']; // We sort the query parameters alphabetically by key so that the hash is always the same const sortedQueryParams = Object.fromEntries(Object.entries(queryParams).sort()); // We build the hash payload by writing the host, path and query parameters into an object const hashData = JSON.stringify({ host: host, path: path || '/', // If there is no path, we use a slash query: new URLSearchParams(sortedQueryParams).toString(), // We build the query parameters back into a query string }); // Next, we calculate the hash of the hash payload and the secret const calculatedSignature = crypto.createHmac('sha256', myApiKey).update(hashData).digest('hex'); // Finally, we compare the calculated signature with the signature from the URL console.log(calculatedSignature === signature); // Expected to be true

This is an example or a proof of concept, and you should adapt it to your programming language, framework, and code style. The important thing is that you calculate the hash of the URL and the secret and compare it to the signature in the URL.

⚠️ Always validate URL signatures

You should always validate the signature of a signed URL before using its parameters. If the signature isn't valid, you shouldn't trust the URL and take appropriate action.