If you’ve tried to send emails from Cloudflare Workers using Azure Communication Services (ACS) and received a frustrating error, you’re not alone. This guide will show you exactly how to fix it.401 "Denied by the resource provider"
Table of Contents
The Problem
Azure Communication Services Email API requires HMAC-SHA256 authentication – it does NOT accept simple API key authentication like many other services. The accesskey in your connection string is not an API key; it’s a cryptographic key used to generate HMAC signatures.
When you try something like this:
// ❌ This will NOT work!
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"api-key": accessKey, // Wrong!
"Content-Type": "application/json"
},
body: JSON.stringify(emailPayload)
});
You’ll get:
{"error":{"code":"Denied","message":"Denied by the resource provider."}}
The Solution
You need to implement proper HMAC-SHA256 authentication using the Web Crypto API (since Node.js crypto module doesn’t work in Cloudflare Workers).
Key Insights
- The access key must be base64 decoded before using it for HMAC signing
- Content hash must be base64 encoded (not hex!)
- Watch for trailing slashes in your endpoint URL
- The string-to-sign format must be exact
Complete Working Code
Here’s the full implementation for a Cloudflare Pages Function:
/**
* Cloudflare Pages Function - Azure Communication Services Email
* with proper HMAC-SHA256 authentication
*/
// Helper: base64 decode
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
// Helper: ArrayBuffer to base64
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// Helper: SHA256 hash (returns base64)
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
return arrayBufferToBase64(hashBuffer);
}
// Helper: HMAC-SHA256 signature (returns base64)
async function hmacSha256(key, message) {
const keyBuffer = base64ToArrayBuffer(key);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const msgBuffer = new TextEncoder().encode(message);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, msgBuffer);
return arrayBufferToBase64(signature);
}
export async function onRequestPost(context) {
const { request, env } = context;
try {
const body = await request.json();
const { name, email, subject, message } = body;
// Parse ACS connection string
const connectionString = env.ACS_CONNECTION_STRING;
const endpointRaw = connectionString.match(/endpoint=([^;]+)/)?.[1];
const accessKey = connectionString.match(/accesskey=([^;]+)/)?.[1];
if (!endpointRaw || !accessKey) {
throw new Error("Invalid ACS connection string format");
}
// IMPORTANT: Remove trailing slash to prevent double slash in URL
const endpoint = endpointRaw.replace(/\/$/, '');
const apiVersion = "2023-03-31";
const apiUrl = `${endpoint}/emails:send?api-version=${apiVersion}`;
const urlObj = new URL(apiUrl);
const host = urlObj.host;
const pathAndQuery = urlObj.pathname + urlObj.search;
// Prepare email payload
const emailPayload = {
senderAddress: env.ACS_SENDER_EMAIL,
content: {
subject: subject,
plainText: message,
html: `<p>${message}</p>`
},
recipients: {
to: [{ address: env.ACS_RECIPIENT_EMAIL }]
},
replyTo: [{ address: email }]
};
const emailBody = JSON.stringify(emailPayload);
// Generate HMAC-SHA256 authentication
const timestamp = new Date().toUTCString();
const contentHash = await sha256(emailBody);
// Create string to sign
// Format: METHOD\nPATH_AND_QUERY\nTIMESTAMP;HOST;CONTENT_HASH
const stringToSign = `POST\n${pathAndQuery}\n${timestamp};${host};${contentHash}`;
// Generate HMAC signature
const signature = await hmacSha256(accessKey, stringToSign);
// Build Authorization header
const authHeader = `HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=${signature}`;
// Send email via ACS
const acsResponse = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Host": host,
"x-ms-date": timestamp,
"x-ms-content-sha256": contentHash,
"Authorization": authHeader
},
body: emailBody
});
if (!acsResponse.ok) {
const errorDetails = await acsResponse.text();
return new Response(JSON.stringify({
error: "Email send failed",
status: acsResponse.status,
details: errorDetails
}), { status: 500 });
}
return new Response(JSON.stringify({
success: true,
message: "Email sent successfully"
}), { status: 200 });
} catch (err) {
return new Response(JSON.stringify({
error: "Internal server error",
details: err.message
}), { status: 500 });
}
}
Common Pitfalls and Fixes
1. Content Hash in Hex Format
Wrong:
// ❌ Hex format
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
Correct:
// ✅ Base64 format return arrayBufferToBase64(hashBuffer);
2. Trailing Slash in Endpoint
Azure connection strings often end with a trailing slash:
endpoint=https://your-acs.communication.azure.com/;accesskey=...
This causes a double slash in your URL:
https://your-acs.communication.azure.com//emails:send ❌
Fix:
const endpoint = endpointRaw.replace(/\/$/, '');
3. Access Key Not Decoded
The access key is base64 encoded in the connection string. You must decode it before using it for HMAC:
// ✅ Decode the key first
const keyBuffer = base64ToArrayBuffer(accessKey);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer, // Use decoded key
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
4. Missing Host Header
Even though fetch sets the Host header automatically, you should explicitly include it since you’re signing it:
headers: {
"Host": host, // Include explicitly
// ... other headers
}
Testing Your Implementation
Before deploying to Cloudflare, test with Node.js locally:
const crypto = require('crypto');
const connectionString = "your-connection-string";
const endpoint = connectionString.match(/endpoint=([^;]+)/)?.[1].replace(/\/$/, '');
const accessKey = connectionString.match(/accesskey=([^;]+)/)?.[1];
const emailPayload = {
senderAddress: "[email protected]",
recipients: { to: [{ address: "[email protected]" }] },
content: {
subject: "Test",
plainText: "Hello World"
}
};
const emailBody = JSON.stringify(emailPayload);
const timestamp = new Date().toUTCString();
const contentHash = crypto.createHash('sha256').update(emailBody).digest('base64');
const url = `${endpoint}/emails:send?api-version=2023-03-31`;
const urlObj = new URL(url);
const pathAndQuery = urlObj.pathname + urlObj.search;
const stringToSign = `POST\n${pathAndQuery}\n${timestamp};${urlObj.host};${contentHash}`;
const signature = crypto.createHmac('sha256', Buffer.from(accessKey, 'base64'))
.update(stringToSign)
.digest('base64');
console.log("Test with this curl command:");
console.log(`curl -X POST '${url}' \\
-H 'Content-Type: application/json' \\
-H 'Host: ${urlObj.host}' \\
-H 'x-ms-date: ${timestamp}' \\
-H 'x-ms-content-sha256: ${contentHash}' \\
-H 'Authorization: HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=${signature}' \\
-d '${emailBody}'`);
Environment Variables
Set these secrets in your Cloudflare Pages dashboard:
: Your full Azure Communication Services connection stringACS_CONNECTION_STRING: Your verified sender email (e.g.,ACS_SENDER_EMAIL[email protected]): Where contact form emails should be sentACS_RECIPIENT_EMAIL
Bonus: Office 365 Anti-Phishing
If your emails land in Office 365 quarantine with “Impersonation domain” detection, update your sender display name:
az rest --method PUT \
--url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Communication/emailServices/{name}/domains/{domain}/senderUsernames/{username}?api-version=2023-04-01" \
--body '{"properties":{"username":"noreply","displayName":"Your Company | Contact Form"}}'
Don’t use the domain name as the display name (e.g., “yourdomain.com”) – this triggers phishing detection.
Conclusion
Azure Communication Services is powerful but its authentication can be tricky, especially in serverless environments like Cloudflare Workers. The key points to remember:
- Use HMAC-SHA256, not simple API keys
- Base64 decode the access key
- Base64 encode the content hash
- Watch for trailing slashes
- Include all required headers
With this implementation, you’ll have reliable email sending from your Cloudflare Workers without the dreaded 401 errors.