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

PracticeDescription
Use Managed IdentityAvoid storing storage keys in APIM
Set content typeProperly set Content-Type header
Add validationValidate file types and sizes
Enable loggingTrack all storage operations
Use compressionConsider gzip for large responses

Azure Integration Hub - Advanced Level