Azure API Management — APIM → Storage Upload/Download
Proxy Blob Operations via Managed Identity Policy
Introduction
Using Azure API Management as a proxy for Azure Blob Storage enables you to:
- Secure storage access — No exposed storage keys to clients
- Centralized control — Apply policies (validation, transformation, logging)
- Custom workflows — Add processing before/after storage operations
- Cross-origin access — Enable browser-based applications to access storage
This pattern is perfect for:
- Document management systems
- User file uploads/downloads
- Image/media serving
- Data export/import APIs
Architecture
┌──────────┐ ┌──────────┐ ┌─────────────┐ ┌────────────┐
│ Client │───▶│ APIM │───▶│ Managed │───▶│ Blob │
│ Browser │ │ Policy │ │ Identity │ │ Storage │
└──────────┘ └──────────┘ └─────────────┘ └────────────┘
│
│ Upload: POST /files
│ Download: GET /files/{name}
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Policy Processing │
│ • Validate request │
│ • Generate SAS token or use MI │
│ • Transform headers/body │
│ • Log operation │
└─────────────────────────────────────────────────────────────┘
Prerequisites
Enable Managed Identity in APIM
# Enable system-assigned managed identity
az apim identity assign \
--name my-apim \
--resource-group my-rg
Grant Blob Storage Permissions
# Get APIM's principal ID
APIM_PRINCIPAL_ID=$(az apim show \
--name my-apim \
--resource-group my-rg \
--query identity.principalId -o tsv)
# Assign Storage Blob Data Contributor role
az role assignment create \
--assignee $APIM_PRINCIPAL_ID \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage
Upload Policy (Client to Storage)
Direct Upload with MI
<policies>
<inbound>
<base />
<!-- Validate file metadata -->
<validate-request header="Content-Type">
<allowed-values>
<value>application/json</value>
<value>image/png</value>
<value>image/jpeg</value>
<value>application/pdf</value>
</allowed-values>
</validate-request>
<set-variable name="fileName" value="@(context.Request.MatchedParameters["fileName"])" />
<set-variable name="contentType" value="@(context.Request.Headers["Content-Type"].FirstOrDefault())" />
</inbound>
<backend>
<!-- Get Managed Identity Token -->
<send-request mode="new" timeout="30" response-variable-name="token-response">
<set-url>http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://storage.azure.com/</set-url>
<set-method>GET</set-method>
<set-header name="Metadata" exists-action="override">true</set-header>
</send-request>
<set-variable name="mi-token" value="@{
var tokenResponse = context.Variables["token-response"] as IResponse;
var tokenBody = tokenResponse.Body.As<JsonElement>();
return tokenBody.GetProperty("access_token").GetString();
}" />
<!-- Upload to Blob Storage -->
<send-request mode="new" timeout="300" response-variable-name="upload-response">
<set-url>@($"https://mystorage.blob.core.windows.net/documents/{context.Variables["fileName"]}")</set-url>
<set-method>PUT</set-method>
<set-header name="x-ms-blob-type" exists-action="override">BlockBlob</set-header>
<set-header name="x-ms-content-md5" exists-action="override">@{
// Compute content MD5 for integrity
var body = context.Request.Body.As<string>();
var md5 = Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(body)));
return md5;
}</set-header>
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {(string)context.Variables["mi-token"]}")</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>@(context.Variables["contentType"])</value>
</set-header>
<set-body>@(context.Request.Body.As<string>())</set-body>
</send-request>
</backend>
<outbound>
<base />
<return-response>
<set-status code="201" reason="Created" />
<set-body>@{
return new {
fileName = context.Variables["fileName"],
status = "uploaded",
url = $"https://mystorage.blob.core.windows.net/documents/{context.Variables["fileName"]}",
uploadedAt = DateTime.UtcNow
};
}</set-body>
</return-response>
</outbound>
</policies>
Download Policy (Storage to Client)
Proxy Download Request
<policies>
<inbound>
<base />
<set-variable name="fileName" value="@(context.Request.MatchedParameters["fileName"])" />
</inbound>
<backend>
<!-- Get MI Token -->
<send-request mode="new" timeout="30" response-variable-name="token-response">
<set-url>http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://storage.azure.com/</set-url>
<set-method>GET</set-header>
<set-header name="Metadata" exists-action="override">true</set-header>
</send-request>
<set-variable name="mi-token" value="@{
var tokenResponse = (IResponse)context.Variables["token-response"];
return tokenResponse.Body.As<JsonElement>().GetProperty("access_token").GetString();
}" />
<!-- Get blob from Storage -->
<send-request mode="new" timeout="120" response-variable-name="blob-response">
<set-url>@($"https://mystorage.blob.core.windows.net/documents/{context.Variables["fileName"]}")</set-url>
<set-method>GET</set-method>
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {(string)context.Variables["mi-token"]}")</value>
</set-header>
</send-request>
</backend>
<outbound>
<base />
<set-header name="Content-Disposition" exists-action="override">
<value>@($"attachment; filename=\"{context.Variables["fileName"]}\"")</value>
</set-header>
<return-response>
<set-body>@(((IResponse)context.Variables["blob-response"]).Body)</set-body>
</return-response>
</outbound>
</policies>
List Files Policy
Enumerate Container Contents
<backend>
<send-request mode="new" timeout="30" response-variable-name="token-response">
<set-url>http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://storage.azure.com/</set-url>
<set-method>GET</set-method>
<set-header name="Metadata" exists-action="override">true</set-header>
</send-request>
<set-variable name="mi-token" value="@{
var tr = (IResponse)context.Variables["token-response"];
return tr.Body.As<JsonElement>().GetProperty("access_token").GetString();
}" />
<send-request mode="new" timeout="30" response-variable-name="list-response">
<set-url>https://mystorage.blob.core.windows.net/documents?restype=container&comp=list&maxresults=1000</set-url>
<set-method>GET</set-method>
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {(string)context.Variables["mi-token"]}")</value>
</set-header>
</send-request>
</backend>
<outbound>
<base />
<set-body>@{
var response = (IResponse)context.Variables["list-response"];
var xml = response.Body.As<string>();
// Parse XML and extract blob names (simplified)
var doc = System.Xml.Linq.XDocument.Parse(xml);
var blobs = doc.Descendants("Blob")
.Select(b => b.Element("Name")?.Value)
.Where(n => n != null)
.ToList();
return new { files = blobs, count = blobs.Count };
}</set-body>
</outbound>
Using SAS Tokens Instead of MI
Generate Short-Lived SAS
<inbound>
<set-variable name="fileName" value="@(context.Request.MatchedParameters["fileName"])" />
<set-variable name="sas-expiry" value="@(DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds())" />
</inbound>
<backend>
<!-- Generate SAS Token -->
<set-variable name="sas-signature" value="@{
var key = Convert.FromBase64String("storage-account-key");
var accountName = "mystorage";
var blobName = (string)context.Variables["fileName"];
var expiry = (long)context.Variables["sas-expiry"];
var stringToSign = $"GET\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nx-ms-blob-type:BlockBlob\nx-ms-version:2021-06-08\n/{accountName}/documents/{blobName}";
using var hmac = new System.Security.Cryptography.HmacSHA256(key);
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
var signature = Convert.ToBase64String(hash);
return $"sv=2021-06-08&se={expiry}&sr=b&sig={Uri.EscapeDataString(signature)}";
}" />
<!-- Request blob with SAS -->
<send-request mode="new" timeout="120" response-variable-name="download-response">
<set-url>@($"https://mystorage.blob.core.windows.net/documents/{context.Variables["fileName"]}?{(string)context.Variables["sas-signature"]}")</set-url>
<set-method>GET</set-method>
<set-header name="x-ms-blob-type" exists-action="override">BlockBlob</set-header>
</send-request>
</backend>
Delete Operation
Delete a Blob
<backend>
<send-request mode="new" timeout="30" response-variable-name="token-response">
<set-url>http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://storage.azure.com/</set-url>
<set-method>GET</set-method>
<set-header name="Metadata" exists-action="override">true</set-header>
</send-request>
<set-variable name="mi-token" value="@{
var tr = (IResponse)context.Variables["token-response"];
return tr.Body.As<JsonElement>().GetProperty("access_token").GetString();
}" />
<send-request mode="new" timeout="30" response-variable-name="delete-response">
<set-url>@($"https://mystorage.blob.core.windows.net/documents/{context.Request.MatchedParameters["fileName"]}")</set-url>
<set-method>DELETE</set-method>
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {(string)context.Variables["mi-token"]}")</value>
</set-header>
<set-header name="x-ms-delete-snapshots" exists-action="override">include</set-header>
</send-request>
</backend>
Security Enhancements
Validate File Type
<inbound>
<!-- Block dangerous file types -->
<set-variable name="extension" value="@{
var filename = (string)context.Request.MatchedParameters["fileName"];
return Path.GetExtension(filename).ToLowerInvariant();
}" />
<choose>
<when condition="@{
var ext = (string)context.Variables["extension"];
return ext == ".exe" || ext == ".dll" || ext == ".cmd" || ext == ".bat";
}">
<return-response>
<set-status code="403" />
<set-body>File type not allowed</set-body>
</return-response>
</when>
</choose>
</inbound>
Quota Enforcement
<inbound>
<set-variable name="file-size-limit-bytes" value="104857600" /> <!-- 100MB -->
<set-variable name="content-length" value="@{
var header = context.Request.Headers["Content-Length"].FirstOrDefault();
return long.TryParse(header, out var length) ? length : 0;
}" />
<choose>
<when condition="@((long)context.Variables["content-length"] > (long)context.Variables["file-size-limit-bytes"])">
<return-response>
<set-status code="413" />
<set-body>File exceeds maximum allowed size of 100MB</set-body>
</return-response>
</when>
</choose>
</inbound>
Best Practices
| Practice | Description |
|---|---|
| Use Managed Identity | Avoid storing storage keys in APIM |
| Set content type | Properly set Content-Type header |
| Add validation | Validate file types and sizes |
| Enable logging | Track all storage operations |
| Use compression | Consider gzip for large responses |
Azure Integration Hub - Advanced Level