Azure Service Bus Managed Identity Authentication
Secure Messaging Without Connection Strings
Introduction
Managed Identity provides a secure, credential-free way to authenticate Azure Service Bus operations. Instead of storing connection strings with secrets in your application configuration, you can leverage the built-in identity that Azure manages automatically. This approach eliminates the risk of exposed credentials, simplifies secret rotation, and provides better audit trails through Azure Active Directory.
This comprehensive guide covers:
- Identity Types — System vs user-assigned managed identities
- Enable on Resources — Configuring identity on various Azure resources
- Role Assignments — Granting proper RBAC permissions
- Code Implementation — Sending and receiving messages securely
- Queue and Topic Operations — Working with queues, topics, and subscriptions
- Best Practices — Security and operational guidance
Understanding the Architecture
How Managed Identity Works with Service Bus
┌─────────────────────────────────────────────────────────────────────┐
│ MANAGED IDENTITY AUTHENTICATION FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ APPLICATION │ │
│ │ │ │
│ │ ServiceBusClient client = new ServiceBusClient( │ │
│ │ "namespace.servicebus.windows.net", │ │
│ │ new DefaultAzureCredential()); │ │
│ │ │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AZURE ACTIVE DIRECTORY │ │
│ │ │ │
│ │ 1. Request token for Service Bus resource │ │
│ │ 2. Validate managed identity │ │
│ │ 3. Issue access token │ │
│ │ │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SERVICE BUS NAMESPACE │ │
│ │ │ │
│ │ Validate token → Authorize operation │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Queue │ │ Topic │ │ Sub │ │ │
│ │ │ Send/Re │ │ Send/Re │ │ Receive │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Identity Types Comparison
| Feature | System-Assigned | User-Assigned |
|---|
| Lifecycle | Tied to resource | Independent |
| Creation | Automatic when enabled | Pre-created |
| Sharing | Single resource only | Multiple resources |
| Use Case | Single function/app | Shared identity |
| Management | Azure handles | Manual management |
Enable Managed Identity
On Azure Functions
# Enable system-assigned identity on function app
az functionapp update \
--name my-function-app \
--resource-group my-rg \
--identity systemassigned
# Or enable during creation
az functionapp create \
--name my-function-app \
--resource-group my-rg \
--storage-account mystorage \
--plan my-app-service-plan \
--identity systemassigned \
--functions-version 4
# Get the principal ID
az functionapp show \
--name my-function-app \
--resource-group my-rg \
--query identity.principalId \
--output tsv
On Container Apps
az containerapp update \
--name my-container-app \
--resource-group my-rg \
--identity systemassigned
# Get the principal ID
az containerapp show \
--name my-container-app \
--resource-group my-rg \
--query identity.principalId \
--output tsv
On Virtual Machines
# Enable managed identity on VM
az vm identity assign \
--name my-vm \
--resource-group my-rg \
--system-assigned
# Get the principal ID
az vm show \
--name my-vm \
--resource-group my-rg \
--query identity.principalId \
--output tsv
On Azure Kubernetes Service (AKS)
# Enable managed identity on AKS cluster
az aks update \
--name my-aks-cluster \
--resource-group my-rg \
--enable-managed-identity
# Get the principal ID (managed cluster)
az aks show \
--name my-aks-cluster \
--resource-group my-rg \
--query identity.principalId \
--output tsv
# For pod identity, use Azure Identity SDK in application
Grant RBAC Roles
Available Roles
| Role | Permissions | Use Case |
|---|
| Azure Service Bus Data Owner | Full control | Send, receive, manage |
| Azure Service Bus Data Sender | Send messages | Produce messages |
| Azure Service Bus Data Receiver | Receive messages | Consume messages |
| Azure Service Bus Data Listener | Listen (sessions) | Session-based receive |
| Azure Service Bus Data Sender | Send to topics | Publish to topics |
Role Assignment at Namespace Level
# Get the managed identity principal ID
IDENTITY_ID=$(az functionapp show \
--name my-function-app \
--resource-group my-rg \
--query identity.principalId \
--output tsv)
# Assign Data Owner (full access)
az role assignment create \
--assignee $IDENTITY_ID \
--role "Azure Service Bus Data Owner" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace"
# Or assign Sender only (for producers)
az role assignment create \
--assignee $IDENTITY_ID \
--role "Azure Service Bus Data Sender" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace"
# Or assign Receiver only (for consumers)
az role assignment create \
--assignee $IDENTITY_ID \
--role "Azure Service Bus Data Receiver" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace"
Role Assignment at Queue/Topic Level
# Grant access to specific queue
az role assignment create \
--assignee $IDENTITY_ID \
--role "Azure Service Bus Data Sender" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace/queues/orders-queue"
# Grant access to specific topic
az role assignment create \
--assignee $IDENTITY_ID \
--role "Azure Service Bus Data Sender" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace/topics/orders-topic"
# Grant access to specific subscription
az role assignment create \
--assignee $IDENTITY_ID \
--role "Azure Service Bus Data Receiver" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/mynamespace/topics/orders-topic/subscriptions/processor-sub"
ARM Template for Role Assignment
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"resources": [
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(resourceId('Microsoft.Web/sites', 'my-function-app'), 'service-bus-role')]",
"properties": {
"roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751b-4115-d93fb7297428')]",
"principalId": "[reference(resourceId('Microsoft.Web/sites', 'my-function-app'), '2022-09-01', 'full').identity.principalId]",
"scope": "[resourceId('Microsoft.ServiceBus/namespaces', 'mynamespace')]"
}
}
]
}
Send Messages with Managed Identity
Basic Send Operation
using Azure.Messaging.ServiceBus;
using Azure.Identity;
public class ServiceBusSender
{
private readonly string _namespace = "mynamespace.servicebus.windows.net";
[FunctionName("SendOrder")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
var order = new Order
{
OrderId = Guid.NewGuid().ToString(),
CustomerId = "cust-001",
Total = 99.99,
Items = new[] { new OrderItem { ProductId = "prod-1", Quantity = 2 } }
};
// Create client with managed identity
var client = new ServiceBusClient(
_namespace,
new DefaultAzureCredential());
// Create sender for queue
var sender = client.CreateSender("orders-queue");
// Create message
var message = new ServiceBusMessage(JsonSerializer.Serialize(order))
{
ContentType = "application/json",
Subject = "New Order",
MessageId = order.OrderId,
CorrelationId = Guid.NewGuid().ToString(),
Properties =
{
{ "CustomerId", order.CustomerId },
{ "OrderTotal", order.Total },
{ "Source", "api" },
{ "Timestamp", DateTime.UtcNow.ToString("o") }
}
};
// Set scheduled delivery (optional)
// message.ScheduledEnqueueTime = DateTimeOffset.UtcNow.AddMinutes(5);
await sender.SendMessageAsync(message);
_logger.LogInformation("Order {OrderId} sent to queue", order.OrderId);
// Clean up
await sender.DisposeAsync();
await client.DisposeAsync();
return new AcceptedResult(new { orderId = order.OrderId });
}
}
Send to Multiple Queues
public class MultiQueueSender
{
[FunctionName("ProcessOrder")]
public async Task Run(
[ServiceBusTrigger("orders-queue", Connection = "ServiceBusConnection")]
ServiceBusReceivedMessage message)
{
var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
// Create client once
var client = new ServiceBusClient(
"mynamespace.servicebus.windows.net",
new DefaultAzureCredential());
// Send to multiple destinations in parallel
var tasks = new List<Task>
{
SendToQueueAsync(client, "orders-processed", order),
SendToTopicAsync(client, "orders-notifications", order),
SendToDeadLetterCheck(client, order)
};
await Task.WhenAll(tasks);
await client.DisposeAsync();
}
private async Task SendToQueueAsync(ServiceBusClient client, string queueName, Order order)
{
var sender = client.CreateSender(queueName);
var message = new ServiceBusMessage(JsonSerializer.Serialize(order))
{
ContentType = "application/json"
};
await sender.SendMessageAsync(message);
await sender.DisposeAsync();
}
private async Task SendToTopicAsync(ServiceBusClient client, string topicName, Order order)
{
var sender = client.CreateSender(topicName);
var message = new ServiceBusMessage(JsonSerializer.Serialize(order))
{
ContentType = "application/json",
Subject = "OrderProcessed"
};
await sender.SendMessageAsync(message);
await sender.DisposeAsync();
}
}
Send to Topic with Filters
public class TopicPublisher
{
[FunctionName("PublishToTopic")]
public async Task Run(
[ServiceBusTrigger("orders-queue")] ServiceBusReceivedMessage message,
ServiceBusMessageActions actions)
{
var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
var client = new ServiceBusClient(
"mynamespace.servicebus.windows.net",
new DefaultAzureCredential());
var sender = client.CreateSender("orders-topic");
// Create message with custom properties for filtering
var topicMessage = new ServiceBusMessage(JsonSerializer.Serialize(order))
{
ContentType = "application/json",
Subject = "OrderUpdated",
Properties =
{
{ "OrderStatus", order.Status },
{ "CustomerRegion", order.Region },
{ "OrderValue", order.Total },
{ "IsHighPriority", order.Total > 1000 }
}
};
// Set message type for subscription filtering
topicMessage.ApplicationProperties["MessageType"] = order.Status switch
{
"New" => "OrderCreated",
"Shipped" => "OrderShipped",
"Delivered" => "OrderDelivered",
"Cancelled" => "OrderCancelled",
_ => "OrderUpdated"
};
await sender.SendMessageAsync(topicMessage);
await sender.DisposeAsync();
await client.DisposeAsync();
}
}
Receive Messages with Managed Identity
Queue Receiver
public class QueueReceiver
{
[FunctionName("ProcessQueue")]
public async Task Run(
[ServiceBusTrigger(
"orders-queue",
Connection = "ServiceBusConnection",
IsSessionsEnabled = false)]
ServiceBusReceivedMessage message,
ServiceBusMessageActions messageActions)
{
_logger.LogInformation("Processing message: {MessageId}", message.MessageId);
try
{
var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
// Process the order
await ProcessOrderAsync(order);
// Success - complete the message
await messageActions.CompleteMessageAsync(message);
_logger.LogInformation("Order {OrderId} processed successfully", order.OrderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process message {MessageId}", message.MessageId);
// Check retry count and decide next action
if (message.DeliveryCount >= 5)
{
// Max retries - dead letter
await messageActions.DeadLetterMessageAsync(message, new Dictionary<string, object>
{
{ "Error", ex.Message },
{ "ErrorType", ex.GetType().Name },
{ "FailedAt", DateTime.UtcNow.ToString("o") }
});
}
else
{
// Retry
await messageActions.AbandonMessageAsync(message);
}
}
}
}
Session-Aware Receiver
public class SessionReceiver
{
[FunctionName("ProcessSessionQueue")]
public async Task Run(
[ServiceBusTrigger(
"orders-queue",
Connection = "ServiceBusConnection",
IsSessionsEnabled = true, // Enable session processing
SessionHandlerOptions = new SessionHandlerOptions(MaxAutoRenewDuration: TimeSpan.FromMinutes(5))
{
MaxConcurrentSessions = 10,
AutoComplete = false
})]
ServiceBusReceivedMessage[] messages,
ServiceBusMessageActions messageActions)
{
foreach (var message in messages)
{
var sessionId = message.SessionId;
_logger.LogInformation("Processing session: {SessionId}", sessionId);
try
{
var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
await ProcessOrderInSessionAsync(order, sessionId);
await messageActions.CompleteMessageAsync(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed in session {SessionId}", sessionId);
await messageActions.AbandonMessageAsync(message);
}
}
}
}
Topic Subscription Receiver
public class SubscriptionReceiver
{
[FunctionName("ProcessSubscription")]
public async Task Run(
[ServiceBusTrigger(
"orders-topic",
"high-priority-sub", // Subscription name
Connection = "ServiceBusConnection")]
ServiceBusReceivedMessage message)
{
_logger.LogInformation("Received from subscription: {MessageId}", message.MessageId);
// Access subscription-specific metadata
var topicName = message.ApplicationProperties.TryGetValue("TopicName", out var topic)
? topic?.ToString()
: "unknown";
var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
// Process high-priority order
await ProcessHighPriorityOrderAsync(order);
}
private async Task ProcessHighPriorityOrderAsync(Order order)
{
_logger.LogInformation("Processing high-priority order: {OrderId}", order.OrderId);
// Urgent processing logic
await Task.Delay(100);
}
}
Use with Service Bus Processor
public class ServiceBusProcessorExample
{
private readonly ServiceBusClient _client;
private readonly ServiceBusProcessor _processor;
public ServiceBusProcessorExample()
{
_client = new ServiceBusClient(
"mynamespace.servicebus.windows.net",
new DefaultAzureCredential());
_processor = _client.CreateProcessor(
"orders-queue",
new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 10,
AutoComplete = false,
MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5)
});
}
[FunctionName("StartProcessor")]
public Task StartProcessing()
{
_processor.ProcessMessageAsync += async args =>
{
var message = args.Message;
try
{
var order = JsonSerializer.Deserialize<Order>(message.Body.ToString());
await ProcessOrderAsync(order);
await args.CompleteMessageAsync(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Processing failed");
await args.DeadLetterMessageAsync(message);
}
};
_processor.ProcessErrorAsync += args =>
{
_logger.LogError(args.Exception, "Processor error");
return Task.CompletedTask;
};
return _processor.StartProcessingAsync();
}
[FunctionName("StopProcessor")]
public async Task StopProcessing()
{
await _processor.StopProcessingAsync();
await _processor.DisposeAsync();
await _client.DisposeAsync();
}
}
Local Development
Azure CLI Login
# Login to Azure
az login
# Set the subscription
az account set --subscription "your-subscription-id"
# Verify you're logged in
az account show
Environment Setup
// DefaultAzureCredential checks multiple sources in order:
// 1. Managed Identity (when deployed to Azure)
// 2. Visual Studio
// 3. Visual Studio Code
// 4. Azure CLI
// 5. Azure PowerShell
// For local development, ensure at least one is configured
// Option 1: Use Azure CLI
// Run: az login
// Option 2: Create service principal
export AZURE_CLIENT_ID="your-client-id"
export AZURE_CLIENT_SECRET="your-client-secret"
export AZURE_TENANT_ID="your-tenant-id"
VS Code Configuration
// local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"ServiceBusConnection": "__full_namespace_connection__"
}
}
Best Practices
Security Configuration
| Practice | Description |
|---|
| Use least privilege | Assign Data Sender/Receiver instead of Data Owner |
| Scope to specific queues | Don't grant namespace-wide access when not needed |
| Enable system-assigned | Simplifies identity lifecycle management |
| Use user-assigned for AKS | Share identity across multiple pods |
Connection Management
// Good: Create client once and reuse
public class OrderService
{
private readonly ServiceBusClient _client;
private readonly ServiceBusSender _sender;
public OrderService()
{
_client = new ServiceBusClient(
"namespace.servicebus.windows.net",
new DefaultAzureCredential());
_sender = _client.CreateSender("orders-queue");
}
public async Task SendOrderAsync(Order order)
{
// Reuse sender - connections are pooled
await _sender.SendMessageAsync(new ServiceBusMessage(JsonSerializer.Serialize(order)));
}
public void Dispose()
{
_sender.Dispose();
_client.Dispose();
}
}
// Bad: Create client per message
public class BadExample
{
public async Task SendOrderAsync(Order order)
{
var client = new ServiceBusClient("namespace.servicebus.windows.net",
new DefaultAzureCredential());
var sender = client.CreateSender("orders-queue");
await sender.SendMessageAsync(new ServiceBusMessage(JsonSerializer.Serialize(order)));
await sender.DisposeAsync();
await client.DisposeAsync();
}
}
Error Handling
// Always include proper exception handling
public async Task SendWithRetryAsync(Order order, int maxRetries = 3)
{
var client = new ServiceBusClient(
"namespace.servicebus.windows.net",
new DefaultAzureCredential());
var sender = client.CreateSender("orders-queue");
int attempts = 0;
while (attempts < maxRetries)
{
try
{
await sender.SendMessageAsync(new ServiceBusMessage(JsonSerializer.Serialize(order)));
return;
}
catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.ServiceTimeout)
{
attempts++;
if (attempts >= maxRetries) throw;
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempts)));
}
}
}
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|
| AuthenticationFailedException | Missing RBAC role | Assign appropriate role |
| 401 Unauthorized | Token not obtained | Check DefaultAzureCredential |
| 403 Forbidden | Insufficient permissions | Verify role scope |
| CredentialUnavailable | No identity configured | Enable MI on resource |
Debug Commands
# 1. Verify managed identity is enabled
az functionapp show \
--name my-function-app \
--resource-group my-rg \
--query identity
# 2. List role assignments for identity
az role assignment list \
--assignee <principal-id> \
--output table
# 3. Test access with Azure CLI
az servicebus queue show \
--name orders-queue \
--namespace-name my-namespace \
--resource-group my-rg
# 4. Check Azure Activity Log
az monitor activity-log list \
--resource-group my-rg \
--query "[?contains(operationName.value, 'ServiceBus')]"
# 5. Test from local with Azure CLI
az login
az servicebus queue list \
--namespace-name my-namespace
Network Access
// If using private endpoints
var options = new ServiceBusClientOptions
{
TransportType = ServiceBusTransportType.AmqpWebSockets,
RetryOptions = new ServiceBusRetryOptions
{
Mode = ServiceBusRetryMode.Exponential,
MaxRetries = 3
}
};
var client = new ServiceBusClient(
"namespace.servicebus.windows.net",
new DefaultAzureCredential(),
options);
Related Topics
Azure Integration Hub - Intermediate Level