I am running some perf testing using K6 and as part of the tests, i need to connect and upload a file to an Azure Blob Container.
My script is failing authentication due to a mismatch of headers..even though they are the same when i check. I am getting the following error response:
ERRO[0001] Response Body: AuthenticationFailed
Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:XXXXXXX
Time:2025-01-10T22:02:41.3196583ZThe MAC signature found in the HTTP request 'xxxxxxx' is not the same as any computed signature. Server used following string to sign: 'PUT
This is the K6 Script i am running.
Has anyone done this before?
import {
check
} from 'k6';
import http from 'k6/http';
import encoding from 'k6/encoding';
import crypto from 'k6/crypto';
export const options = {
stages: [{
duration: '1m',
target: 1
},
{
duration: '10m',
target: 1
},
{
duration: '1m',
target: 0
},
],
};
// Environment variables
const storageAccount = __ENV.STORAGE_ACCOUNT;
const containerName = __ENV.CONTAINER_NAME;
const accountKey = __ENV.ACCOUNT_KEY;
if (!storageAccount || !containerName || !accountKey) {
throw new Error("Missing STORAGE_ACCOUNT, CONTAINER_NAME, or ACCESS_KEY environment variables.");
}
// Function to compute HMAC SHA256 signature
function signWithAccountKey(stringToSign, accountKey) {
console.log("Raw Account Key:", accountKey);
const decodedKey = encoding.b64decode(accountKey);
console.log("Decoded Account Key (Hex):", Array.from(decodedKey).map((byte) => byte.toString(16).padStart(2, '0')).join('')); // Log decoded key
const hmac = crypto.createHMAC('sha256', decodedKey);
hmac.update(stringToSign, 'utf8');
const signature = hmac.digest('base64');
console.log("Generated Signature:", signature); // Log the generated signature
return signature;
}
// Function to generate the Authorization header
function generateAuthorizationHeader(verb, urlPath, headers) {
const canonicalizedHeaders = Object.keys(headers)
.filter((key) => key.startsWith('x-ms-'))
.sort()
.map((key) => `${key}:${headers[key]}\n`)
.join('');
const canonicalizedResource = `/${storageAccount}${urlPath}`;
const stringToSign = [
verb,
'', // Content-Encoding
'', // Content-Language
headers['Content-Length'] || '', // Content-Length
'', // Content-MD5
headers['Content-Type'] || '', // Content-Type
'', // Date (not used because we use x-ms-date)
'', // If-Modified-Since
'', // If-Match
'', // If-None-Match
'', // If-Unmodified-Since
'', // Range
canonicalizedHeaders,
canonicalizedResource,
].join('\n');
// Log the StringToSign for debugging
console.log("StringToSign:\n" + stringToSign);
const signature = signWithAccountKey(stringToSign, accountKey);
return `SharedKey ${storageAccount}:${signature}`;
}
// Base URL and content
const baseUrl = `https://${storageAccount}.blob.core.windows.net/${containerName}`;
const fileContent = 'A'.repeat(1024 * 1024 * 100); // 100 MB file content
export default function() {
const fileName = `test-${__VU}-${__ITER}.xml`;
const urlPath = `/${containerName}/${fileName}`;
const url = `${baseUrl}/${fileName}`;
const date = new Date().toUTCString();
const headers = {
'x-ms-date': date,
'x-ms-version': '2020-10-02',
'x-ms-blob-type': 'BlockBlob',
'Content-Type': 'application/xml',
'Content-Length': fileContent.length.toString(),
};
headers['Authorization'] = generateAuthorizationHeader('PUT', urlPath, headers);
// Log headers for debugging
console.log("Authorization Header:", headers['Authorization']);
console.log("Headers Sent:", JSON.stringify(headers));
const res = http.put(url, fileContent, {
headers
});
const success = check(res, {
'Upload succeeded': (r) => r.status === 201,
});
if (!success) {
console.error(`Upload failed for ${fileName}`);
console.error(`Status: ${res.status}`);
console.error(`Response Body: ${res.body}`);
console.error(`Headers Sent: ${JSON.stringify(headers)}`);
}
}
Thanks
The error AuthenticationFailedServer failed to authenticate the request
indicates that the Authorization
header is not correctly set for azure azure storage .
You can use the library https://jslib.k6.io/url/1.0.0/index.js
for the URL
module and modify the Authorization
header based on a stringToSign
, which is created by combining different HTTP request components.
This process involves canonicalizing the headers and resources. Additionally, the signature is generated using crypto.hmac
with the decoded account key.
Refer to this doc for URLs with query parameters in K6.
The bellow K6 code is used to upload a file to an Azure Blob Storage container using the Azure Storage REST API:
import http from 'k6/http';
import { check } from 'k6';
import crypto from 'k6/crypto';
import encoding from 'k6/encoding';
import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js';
const STORAGE_ACCOUNT_NAME = 'STORAGE_ACCOUNT_NAME';
const STORAGE_ACCOUNT_KEY = 'STORAGE_ACCOUNT_KEY';
const CONTAINER_NAME = 'ravi'; // Replace with your container name
const FILE_NAME = '793504351.txt';
const FILE_CONTENT = 'Hello, Azure Blob Storage!';
function getAuthorizationHeader(method, url, headers) {
const canonicalizedHeaders = getCanonicalizedHeaders(headers);
const canonicalizedResource = getCanonicalizedResource(url);
const stringToSign = [
method.toUpperCase(),
headers['Content-Encoding'] || '',
headers['Content-Language'] || '',
headers['Content-Length'] || '',
headers['Content-MD5'] || '',
headers['Content-Type'] || '',
headers['Date'] || '',
headers['If-Modified-Since'] || '',
headers['If-Match'] || '',
headers['If-None-Match'] || '',
headers['If-Unmodified-Since'] || '',
headers['Range'] || '',
canonicalizedHeaders,
canonicalizedResource,
].join('\n');
const key = encoding.b64decode(STORAGE_ACCOUNT_KEY, 'std');
const signature = crypto.hmac('sha256', key, stringToSign, 'base64');
return `SharedKey ${STORAGE_ACCOUNT_NAME}:${signature}`;
}
function getCanonicalizedHeaders(headers) {
const xMsHeaders = Object.keys(headers)
.filter((header) => header.startsWith('x-ms-'))
.sort();
return xMsHeaders
.map((header) => `${header}:${headers[header].trim()}`)
.join('\n');
}
function getCanonicalizedResource(url) {
const uri = new URL(url);
let resource = `/${STORAGE_ACCOUNT_NAME}${uri.pathname}`;
return resource;
}
export default function () {
const blobUrl = `https://${STORAGE_ACCOUNT_NAME}.blob.core.windows.net/${CONTAINER_NAME}/${FILE_NAME}`;
const headers = {
'x-ms-date': new Date().toUTCString(),
'x-ms-version': '2021-04-10',
'x-ms-blob-type': 'BlockBlob',
'Content-Type': 'text/plain',
'Content-Length': FILE_CONTENT.length.toString(),
};
headers['Authorization'] = getAuthorizationHeader('PUT', blobUrl, headers);
const res = http.put(blobUrl, FILE_CONTENT, { headers: headers });
check(res, {
'is status 201': (r) => r.status === 201,
});
console.log(`Upload status: ${res.status}`);
if (res.status === 201) {
console.log('File uploaded successfully!');
} else {
console.error(`Upload failed: ${res.status}`);
}
}
Output: