Skip to content

Security

๐Ÿ” Ditching Storage Account Keys: OAuth and Managed Identity for Azure Files REST API

TL;DR

  • โœ… Managed identities can authenticate to Azure Files via REST API using OAuth tokens โ€” no storage account keys required
  • โš ๏ธ The x-ms-file-request-intent: backup header is mandatory โ€” without it, all OAuth requests return HTTP 400
  • ๐ŸŽฏ For OAuth-based access over the Azure Files REST API, assign the Storage File Data Privileged Reader or Storage File Data Privileged Contributor role, scoped appropriately (for example, at the file share level). For SMB access, use the dedicated Storage File Data SMB Share roles instead.
  • ๐Ÿ• OAuth tokens expire after ~1 hour โ€” implement caching and proactive refresh
  • ๐Ÿ“ฆ No additional SMB OAuth configuration is required on the storage account when using OAuth authentication over the REST API.

OAuth-based REST access can be introduced alongside existing Shared Key or SAS usage during migration.


The Problem: Storage Account Keys Are a Liability ๐Ÿ”‘

If you're accessing Azure Files from VMs using storage account keys, you've probably felt the pain:

  • ๐Ÿ”„ Key rotation overhead โ€” someone has to rotate them, update configs, and pray nothing breaks
  • ๐Ÿ”“ Security risk โ€” keys in config files are secrets waiting to be compromised
  • ๐Ÿ•ต๏ธ Limited auditability โ€” logs show "AccessKey" but not which VM or identity made the request
  • ๐Ÿ“‹ Compliance headaches โ€” auditors love asking about secret management

The goal: replace storage account key authentication with managed identity OAuth tokens. No secrets to manage. No keys to rotate. Full identity attribution in logs.

๐Ÿ“– This post is part of my Managed Identity Series โ€” replacing secrets with identity-based authentication across Azure services:

Sounds straightforward, right? ๐Ÿ˜…


Key Concepts ๐Ÿ“š

Managed Identity & IMDS

Managed Identity is an Azure-managed identity attached to your VM. No credentials to store โ€” Azure handles the lifecycle automatically.

IMDS (Instance Metadata Service) is a REST endpoint available only from within Azure VMs at 169.254.169.254. Your VM can request OAuth tokens from IMDS without any pre-configured secrets.

OAuth vs Storage Account Keys

Aspect Storage Account Key Managed Identity OAuth
Authentication SharedKey signature OAuth Bearer token
Authorization Full admin access + RBAC RBAC only
Logging Shows "AccessKey" Shows specific VM identity
Key Management Manual rotation required Automatic (Azure-managed)
Security Keys can be compromised No secrets to manage
Auditing Limited identity tracking Full identity attribution

The Backup Intent Header โ€” The Missing Piece ๐Ÿงฉ

Here's where I lost a day of my life.

The x-ms-file-request-intent: backup header is mandatory for OAuth REST API access to Azure Files.

Without it:

HTTP 400 Bad Request
(No helpful error message)

With it:

HTTP 200 OK
Operations succeed

This header tells Azure Files to use backup semantic permissions, which:

  • When using the privileged backup semantics actions, file and directory-level permissions (such as NTFS ACLs) are bypassed, allowing access regardless of existing ACLs
  • Grants admin-level read/write access to all files
  • Is specifically designed for backup, restore, and auditing scenarios

Microsoft documentation mentions this header but doesn't emphasise just how non-negotiable it is. Every OAuth request will fail without it.


How It Works: Authentication Flow ๐Ÿ”„

Authentication Flow

Azure Files validates:

  1. Token signature and expiration
  2. Token audience (https://storage.azure.com/)
  3. RBAC role assignment for the identity
  4. Presence of the backup intent header

Prerequisites โœ…

Before you start, ensure:

  1. System-assigned managed identity enabled on each VM
  2. RBAC Role: "Storage File Data Privileged Contributor" assigned at share level
  3. Network connectivity: Port 443 (HTTPS) accessible to storage account
  4. Azure IMDS: Accessible from VMs (169.254.169.254)
  5. API Version: 2022-11-02 or later (2023-11-03 recommended)

๐Ÿ’ก Note: RBAC propagation can take 5-30 minutes after assignment. Testing immediately after assigning a role may fail โ€” give it time.


Implementation Deep Dive ๐Ÿ› ๏ธ

Step 1 โ€” Token Acquisition from IMDS

Request an OAuth token from the Instance Metadata Service:

Endpoint:

GET http://169.254.169.254/metadata/identity/oauth2/token

Query Parameters:

  • api-version=2018-02-01 (or later)
  • resource=https://storage.azure.com/ (note the trailing slash)

Required Header:

  • Metadata: true

Example Response:

{
  "access_token": "eyJ0eXAi...",
  "expires_on": "1737475200",
  "resource": "https://storage.azure.com/",
  "token_type": "Bearer"
}

PowerShell Example:

$tokenResponse = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/" -Headers @{Metadata="true"}
$token = $tokenResponse.access_token

Step 2 โ€” Required REST API Headers

Every REST API call to Azure Files must include these three headers:

Header Value Notes
Authorization Bearer <access_token> The token from IMDS
x-ms-version 2023-11-03 API version 2022-11-02 or later required
x-ms-file-request-intent backup MANDATORY โ€” requests fail without this

PowerShell Example:

$headers = @{
    "Authorization" = "Bearer $token"
    "x-ms-version" = "2023-11-03"
    "x-ms-file-request-intent" = "backup"
}

Step 3 โ€” Token Lifecycle Management

OAuth tokens expire after approximately 1 hour. Your application must handle this.

Token Refresh Strategy:

  • Cache the token and its expires_on timestamp
  • Refresh proactively (e.g., 5 minutes before expiration)
  • Ensure thread-safe access to cached token if multi-threaded

Pseudocode:

function getToken():
    if cachedToken is null OR cachedToken.expiresOn < (now + 5 minutes):
        cachedToken = fetchTokenFromIMDS()
    return cachedToken.accessToken

Step 4 โ€” Error Handling

HTTP Code Meaning Action
400 Bad Request Missing x-ms-file-request-intent: backup header Add the header to all requests
401 Unauthorized Expired or invalid token Refresh token from IMDS, retry once
403 Forbidden Valid token but insufficient RBAC permissions Do not retry โ€” this is a permissions issue

Critical: If you get HTTP 401, invalidate your cached token, acquire a fresh one, and retry the request once. If it fails again, treat it as an authorization failure.


Worked Example: PowerShell Test Script ๐Ÿ’ป

Here's a complete test script that validates managed identity access:

<#
.SYNOPSIS
    Test managed identity REST API access to Azure Files
.DESCRIPTION
    Tests system-assigned MI can access file share via REST API with OAuth
.EXAMPLE
    .\Test-MIAccess.ps1 -StorageAccountName "yourstorageaccount" -ShareName "yourfileshare"
#>

param(
    [Parameter(Mandatory=$true)]
    [string]$StorageAccountName,

    [Parameter(Mandatory=$true)]
    [string]$ShareName
)

Write-Host "`n=== Testing Managed Identity Access ===" -ForegroundColor Cyan
Write-Host "VM: $env:COMPUTERNAME"
Write-Host "Storage: $StorageAccountName"
Write-Host "Share: $ShareName`n"

# Get Token
Write-Host "[1/3] Getting token..." -NoNewline
try {
    $token = (Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/" -Headers @{Metadata="true"} -TimeoutSec 10).access_token
    Write-Host " OK" -ForegroundColor Green
} catch {
    Write-Host " FAILED" -ForegroundColor Red
    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
    exit 1
}

# Headers with backup intent (CRITICAL)
$headers = @{
    "Authorization" = "Bearer $token"
    "x-ms-version" = "2023-11-03"
    "x-ms-file-request-intent" = "backup"
}

# Test: Create directory
Write-Host "[2/3] Creating test directory..." -NoNewline
try {
    Invoke-RestMethod -Uri "https://$StorageAccountName.file.core.windows.net/$ShareName/mi-test?restype=directory" -Headers $headers -Method PUT | Out-Null
    Write-Host " OK" -ForegroundColor Green
} catch {
    if ($_.Exception.Response.StatusCode.value__ -eq 409) {
        Write-Host " EXISTS" -ForegroundColor Yellow
    } else {
        Write-Host " FAILED" -ForegroundColor Red
        Write-Host "Error: HTTP $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red
        exit 1
    }
}

# Test: Write file
Write-Host "[3/3] Writing test file..." -NoNewline
try {
    $testFile = "test-$env:COMPUTERNAME.txt"
    $testContent = "Test from $env:COMPUTERNAME at $(Get-Date)"
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($testContent)

    # Create file
    $createHeaders = $headers.Clone()
    $createHeaders["x-ms-content-length"] = $bytes.Length
    $createHeaders["x-ms-type"] = "file"
    Invoke-RestMethod -Uri "https://$StorageAccountName.file.core.windows.net/$ShareName/mi-test/$testFile" -Headers $createHeaders -Method PUT | Out-Null

    # Write content
    $writeHeaders = $headers.Clone()
    $writeHeaders["x-ms-range"] = "bytes=0-$($bytes.Length - 1)"
    $writeHeaders["x-ms-write"] = "update"
    Invoke-RestMethod -Uri "https://$StorageAccountName.file.core.windows.net/$ShareName/mi-test/$testFile`?comp=range" -Headers $writeHeaders -Method PUT -Body $bytes | Out-Null

    Write-Host " OK" -ForegroundColor Green
    Write-Host "`nFile created: mi-test/$testFile" -ForegroundColor Gray
} catch {
    Write-Host " FAILED" -ForegroundColor Red
    Write-Host "Error: HTTP $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red
    exit 1
}

Write-Host "`n=== SUCCESS ===" -ForegroundColor Green
Write-Host "Managed identity can access the file share via REST API.`n"
exit 0

Expected Output:

=== Testing Managed Identity Access ===
VM: YOURVM01
Storage: yourstorageaccount
Share: yourshare

[1/3] Getting token... OK
[2/3] Creating test directory... OK
[3/3] Writing test file... OK

File created: mi-test/test-YOURVM01.txt

=== SUCCESS ===
Managed identity can access the file share via REST API.

Common Pitfalls and How to Avoid Them ๐Ÿšง

1. HTTP 400 Bad Request (No Error Details)

Cause: Missing x-ms-file-request-intent: backup header

Solution: Add the header to every REST API call. This is non-negotiable for OAuth.

๐Ÿ’ก This is the most common issue and the least helpful error message. If you're getting 400s with OAuth tokens, check this header first.

2. HTTP 403 Forbidden

Cause: RBAC role not assigned or not yet propagated

Solution:

  • Verify role assignment exists at the correct scope (share level)
  • Wait 5-30 minutes after new role assignments
  • Check the managed identity's Object ID matches the assignment

3. SMB OAuth vs REST API OAuth Confusion

SMB OAuth (EnableSmbOAuth) is for mounting shares via SMB protocol (UNC paths like \\storage.file.core.windows.net\share).

REST API OAuth is for HTTPS endpoints (https://storage.file.core.windows.net/share/...).

These are completely separate features. If you're using REST API calls, you don't need EnableSmbOAuth on the storage account.

๐Ÿ’ก Side note: I actually enabled EnableSmbOAuth on the same storage account during this investigation, thinking it might be required. It wasn't โ€” and I couldn't get SMB OAuth working regardless. The SMB OAuth feature requires additional prerequisites (non-domain-joined VMs, the AzFilesSMBMIClient PowerShell module, etc.) that didn't apply to my use case. That's a separate rabbit hole for another day. ๐Ÿ‡

4. Token Acquisition Fails

Cause: Managed identity not enabled on VM, or IMDS not accessible

Solution:

  • Verify system-assigned managed identity is enabled in Azure Portal
  • Test IMDS access: Invoke-RestMethod -Uri "http://169.254.169.254/metadata/instance?api-version=2021-02-01" -Headers @{Metadata="true"}

5. Testing Immediately After RBAC Assignment

Cause: RBAC changes take time to propagate

Solution: Wait 5-30 minutes before testing. Azure's control plane needs time to replicate permissions.


Checklist: What to Do Next โœ…

Infrastructure Setup

  • [ ] Enable system-assigned managed identity on each VM
  • [ ] Assign "Storage File Data Privileged Contributor" role at share level
  • [ ] Wait 5-30 minutes for RBAC propagation
  • [ ] Test with PowerShell script above

Application Code Changes

  • [ ] Implement IMDS token acquisition
  • [ ] Add all three required headers to REST calls
  • [ ] Implement token caching with proactive refresh
  • [ ] Add HTTP 401 retry logic (refresh token, retry once)
  • [ ] Remove storage account keys from configuration

Rollout

  • [ ] Test on one VM first
  • [ ] Monitor StorageFileLogs for OAuth authentication
  • [ ] Deploy to remaining VMs (staged rollout)
  • [ ] After all VMs migrated, disable storage account key access:
Set-AzStorageAccount -ResourceGroupName "your-rg" `
    -Name "yourstorageaccount" `
    -AllowSharedKeyAccess $false

References ๐Ÿ“š


Final Thoughts ๐Ÿ’ญ

This should have been straightforward. Microsoft's documentation covers all the pieces โ€” managed identities, OAuth tokens, Azure Files REST API. But the critical detail (that x-ms-file-request-intent: backup header) is easy to miss, and the error messages don't help.

Once you know the trick, the implementation is clean:

  1. Get token from IMDS
  2. Add three headers to your requests
  3. Cache and refresh tokens

No more keys in config files. No more rotation schedules. Full identity attribution in your logs.

If you're still using storage account keys for REST API access to Azure Files โ€” now you don't have to.


๐Ÿ“Œ Published on: 2026-01-23 โณ Read time: 8 min

Share on Share on

Replacing SQL Credentials with User Assigned Managed Identity (UAMI) in Azure SQL Managed Instance

Storing SQL usernames and passwords in application configuration files is still common practice โ€” but it poses a significant security risk. As part of improving our cloud security posture, I recently completed a project to eliminate plain text credentials from our app connection strings by switching to Azure User Assigned Managed Identity (UAMI) authentication for our SQL Managed Instance.

In this post, Iโ€™ll walk through how to:

  • Securely connect to Azure SQL Managed Instance without using usernames or passwords
  • Use a User Assigned Managed Identity (UAMI) for authentication
  • Test this connection using the new Go-based sqlcmd CLI
  • Update real application code to remove SQL credentials

๐Ÿ” Why Replace SQL Credentials?

Hardcoded SQL credentials come with several downsides:

  • Security risk: Stored secrets can be compromised if not properly secured
  • Maintenance overhead: Rotating passwords across environments is cumbersome
  • Audit concerns: Plain text credentials often trigger compliance red flags

Azure Managed Identity solves this by providing a token-based, identity-first way to connect to services โ€” no secrets required.

๐Ÿ“– This post is part of my Managed Identity Series โ€” replacing secrets with identity-based authentication across Azure services:


โš™๏ธ What is a User Assigned Managed Identity?

There are two types of Managed Identities in Azure:

  • System-assigned: Tied to the lifecycle of a specific resource (like a VM or App Service)
  • User-assigned: Standalone identity that can be attached to one or more resources

For this project, we used a User Assigned Managed Identity (UAMI) to allow our applications to authenticate against SQL without managing secrets.


๐ŸŒŸ Project Objective

Replace plain text SQL credentials in application connection strings with User Assigned Managed Identity (UAMI) for secure, best-practice authentication to Azure SQL Managed Instances.


โœ… Prerequisites

To follow this guide, youโ€™ll need:

  • An Azure SQL Managed Instance with Microsoft Entra (AAD) authentication enabled
  • A User Assigned Managed Identity (UAMI)
  • An Azure VM or App Service to host your app (or test client)
  • The Go-based sqlcmd CLI installed
    โ†’ Install guide

๐Ÿ”ง Setting Up the User Assigned Managed Identity (UAMI)

Before connecting to Azure SQL using UAMI, ensure the following steps are completed:

  • Create the UAMI
  • Assign the UAMI to the Virtual Machine(s)
  • Configure Microsoft Entra authentication on the SQL Managed Instance
  • Grant SQL access to the UAMI

These steps can be completed via Azure CLI, PowerShell, or the Azure Portal.


๐Ÿ› ๏ธ Step 1: Create the User Assigned Managed Identity (UAMI)

โœ… CLI
az identity create \
  --name my-sql-uami \
  --resource-group my-rg \
  --location <region>

Save the Client ID and Object ID โ€” youโ€™ll need them later.

โœ… Portal
  1. Go to Azure Portal โ†’ Search Managed Identities
  2. Click + Create
  3. Choose Subscription, Resource Group, and Region
  4. Name the identity (e.g., my-sql-uami)
  5. Click Review + Create

๐Ÿ–‡๏ธ Step 2: Assign the UAMI to a Virtual Machine

Attach the UAMI to:

  • The VM(s) running your application code
  • The VM used to test the connection
โœ… CLI
az vm identity assign \
  --name my-vm-name \
  --resource-group my-rg \
  --identities my-sql-uami
โœ… Portal
  1. Go to Virtual Machines โ†’ Select your VM
  2. Click Identity under Settings
  3. Go to the User assigned tab
  4. Click + Add โ†’ Select the UAMI
  5. Click Add

๐Ÿ”‘ Step 3: Configure SQL Managed Instance for Microsoft Entra Authentication

  1. Set an Entra Admin:
  2. Go to your SQL MI โ†’ Azure AD admin blade
  3. Click Set admin and choose a user or group
  4. Save changes

  5. Ensure Directory Reader permissions:

  6. Your SQL MIโ€™s managed identity needs Directory Reader access
  7. You can assign this role via Entra ID > Roles and administrators > Directory Readers

More details: Configure Entra authentication


๐Ÿ“œ Step 4: (Optional) Assign Azure Role to the UAMI

This may be needed if the identity needs to access Azure resource metadata or use Azure CLI from the VM.

โœ… CLI
az role assignment create \
  --assignee-object-id <uami-object-id> \
  --role "Reader" \
  --scope /subscriptions/<sub-id>/resourceGroups/<rg-name>
โœ… Portal
  1. Go to the UAMI โ†’ Azure role assignments
  2. Click + Add role assignment
  3. Choose role (e.g., Reader)
  4. Set scope
  5. Click Save

๐Ÿ”‘ Step 5: Grant SQL Access to the UAMI

Once the UAMI is assigned to the VM and Entra auth is enabled on SQL MI, log in with an admin and run:

CREATE USER [<client-id>] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [<client-id>];
ALTER ROLE db_datawriter ADD MEMBER [<client-id>];

Or use a friendly name:

CREATE USER [my-app-identity] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [my-app-identity];

๐Ÿงช Step 6: Test the Connection Using sqlcmd

sqlcmd \
  -S <your-sql-mi>.database.windows.net \
  -d <database-name> \
  --authentication-method ActiveDirectoryManagedIdentity \
  -U <client-id-of-uami>

If successful, youโ€™ll see the 1> prompt where you can execute SQL queries.


๐Ÿ“Š Step 7: Update Application Code

Update your app to use the UAMI for authentication.

Example connection string for UAMI in C#:

string connectionString = @"Server=tcp:<your-sql-mi>.database.windows.net;" +
                          "Authentication=Active Directory Managed Identity;" +
                          "Encrypt=True;" +
                          "User Id=<your-uami-client-id>;" +
                          "Database=<your-db-name>;";

Make sure your code uses Microsoft.Data.SqlClient with AAD token support.

Or retrieve and assign the token programmatically:

var credential = new DefaultAzureCredential();
var token = await credential.GetTokenAsync(new TokenRequestContext(
    new[] { "https://database.windows.net/" }));

var connection = new SqlConnection("Server=<your-sql-mi>; Database=<your-db-name>; Encrypt=True;");
connection.AccessToken = token.Token;

๐Ÿ”’ Security Benefits

  • ๐Ÿ” No credentials stored
  • ๐Ÿ” No password rotation
  • ๐Ÿ›ก๏ธ Entra-integrated access control and auditing

โœ… Summary

By switching to User Assigned Managed Identity, we removed credentials from connection strings and aligned SQL access with best practices for cloud identity and security.

Comments and feedback welcome!

Share on Share on