javascriptazureazure-blob-storagek6

K6 - Azure Blob Storage Authentication


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: AuthenticationFailedServer 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


Solution

  • 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:

    k6 console

    Azure storage