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

FeatureSystem-AssignedUser-Assigned
LifecycleTied to resourceIndependent
CreationAutomatic when enabledPre-created
SharingSingle resource onlyMultiple resources
Use CaseSingle function/appShared identity
ManagementAzure handlesManual 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

RolePermissionsUse Case
Azure Service Bus Data OwnerFull controlSend, receive, manage
Azure Service Bus Data SenderSend messagesProduce messages
Azure Service Bus Data ReceiverReceive messagesConsume messages
Azure Service Bus Data ListenerListen (sessions)Session-based receive
Azure Service Bus Data SenderSend to topicsPublish 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

PracticeDescription
Use least privilegeAssign Data Sender/Receiver instead of Data Owner
Scope to specific queuesDon't grant namespace-wide access when not needed
Enable system-assignedSimplifies identity lifecycle management
Use user-assigned for AKSShare 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

IssueCauseSolution
AuthenticationFailedExceptionMissing RBAC roleAssign appropriate role
401 UnauthorizedToken not obtainedCheck DefaultAzureCredential
403 ForbiddenInsufficient permissionsVerify role scope
CredentialUnavailableNo identity configuredEnable 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