Authenticating Event Hubs publishers with SAS Token

Event Hub is easy to use, highly scalable Azure service to distribute messages.
However, it's not good idea to "share" the connection string and/or SAS token with multiple senders.

Event Hub can issue SAS token for each publisher to solve this security challenge. We can also validate if the message comes from expected publisher when handling message.

In this article, I will create C# console application to test this feature.

Create Event Hub

1. Create Event Hubs Namespace and select at least 'standard' tier as 'basic' tier doesn't support this feature.

2. Add an Event Hub. I named it 'publisher-test'.
image

3. Select "Shared access policies" and add new one.

4. Create key with "Send" policy. I name the policy as "Send". Note the primary key.

5. We also need blob storage to consume EventHub. Add new storage account and note connection string.

6. Create 'checkpoint' blob container.

Create EventHub Publisher app

I use C# to issue SAS Token and send message, but you can use whatever language you want by referencing official doc examples.

1. Create new console app.

dotnet new console -n eventhubpublishertest
cd eventhubpublishertest

2. Open with any editor. I use Visual Studio Code here.

. code

3. Update Program.cs. You can change Token expiry by updating code. I set 'publisher1' as publisher name in this example.

using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace eventhubpublishertest
{
    class Program
    {
        static void Main(string[] args)
        {
            var eventHubHostName = "kenakamupublishertest.servicebus.windows.net";
            var eventHubName = "publisher-test";
            var keyName = "Send";
            var keyValue = "<your send policy key>";
            var publisher = "publisher1";
            var sasToken = CreateToken($"https://{eventHubHostName}/{eventHubName}/publishers/{publisher}", keyName, keyValue);
        }

        private static string CreateToken(string resourceUri, string keyName, string key)
        {
            TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
            var week = 60 * 60 * 24 * 7;
            var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
            string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
            HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
            var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
            var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName);
            return sasToken;
        }
    }
}

4. Add Azure.Messaging.EventHubs to the project.

dotnet add package Azure.Messaging.EventHubs

5. Update Program.cs to add EventHubProducerClient.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Azure;
using Azure.Messaging.EventHubs;
using Azure.Messaging.EventHubs.Producer;

namespace eventhubpublishertest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var eventHubHostName = "kenakamupublishertest.servicebus.windows.net";
            var eventHubName = "publisher-test";
            var keyName = "Send";
            var keyValue = "<your send policy key>";
            var publisher = "publisher1";
            var sasToken = CreateToken($"https://{eventHubHostName}/{eventHubName}/publishers/{publisher}", keyName, keyValue);

            var producerClient = new EventHubProducerClient(
                                eventHubHostName,
                                $"{eventHubName}/publishers/{publisher}",
                                new AzureSasCredential(sasToken));

            var sendEvents = new List<EventData>()
            {
                new EventData(Encoding.ASCII.GetBytes("test")),
            };
            await producerClient.SendAsync(sendEvents).ConfigureAwait(false);
        }

        private static string CreateToken(string resourceUri, string keyName, string key)
        {
            TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
            var week = 60 * 60 * 24 * 7;
            var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
            string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
            HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
            var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
            var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName);
            return sasToken;
        }
    }
}

Create EventHub consumer app

1. Create new console app.

dotnet new console -n eventhubconsumertest
cd eventhubconsumertest

2. Add event hub packages.

dotnet add package Azure.Messaging.EventHubs
dotnet add package Azure.Messaging.EventHubs.Processor

3. Update Program.cs. I take the base code from Receive Events.

using System;
using System.Text;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Messaging.EventHubs;
using Azure.Messaging.EventHubs.Consumer;
using Azure.Messaging.EventHubs.Processor;

namespace eventhubconsumertest
{
    class Program
    {
        private const string ehubNamespaceConnectionString = "<event hub namespace connection string>";
        private const string eventHubName = "publisher-test";
        private const string blobStorageConnectionString = "<storage account connection string>";
        private const string blobContainerName = "checkpoint";
        static BlobContainerClient storageClient;
        static EventProcessorClient processor;

        static async Task Main(string[] args)
        {
            string consumerGroup = EventHubConsumerClient.DefaultConsumerGroupName;
            storageClient = new BlobContainerClient(blobStorageConnectionString, blobContainerName);
            processor = new EventProcessorClient(storageClient, consumerGroup, ehubNamespaceConnectionString, eventHubName);
            processor.ProcessEventAsync += ProcessEventHandler;
            processor.ProcessErrorAsync += ProcessErrorHandler;

            await processor.StartProcessingAsync();
            await Task.Delay(TimeSpan.FromSeconds(30));
            await processor.StopProcessingAsync();
        }

        static async Task ProcessEventHandler(ProcessEventArgs eventArgs)
        {
            // Write the body of the event to the console window
            Console.WriteLine("\tReceived event: {0}", Encoding.UTF8.GetString(eventArgs.Data.Body.ToArray()));
            Console.WriteLine("\tPublisher: {0}", eventArgs.Data.PartitionKey);

            // Update checkpoint in the blob storage so that the app receives only new events the next time it's run
            await eventArgs.UpdateCheckpointAsync(eventArgs.CancellationToken);
        }

        static Task ProcessErrorHandler(ProcessErrorEventArgs eventArgs)
        {
            // Write details about the error to the console window
            Console.WriteLine($"\tPartition '{ eventArgs.PartitionId}': an unhandled exception was encountered. This was not expected to happen.");
            Console.WriteLine(eventArgs.Exception.Message);
            return Task.CompletedTask;
        }
    }
}

Test

1. Run publisher app first.

dotnet run

2. Run consumer app second to see the result.

dotnet run

Summary

SAS Token for each publisher makes sense from security point of view. We cannot send message if we use different combination of token and publisher. Also consumer can confirm where this message comes from.

41