What Was I Thinking?

Follies & Foils of .NET Development
posts - 94 , comments - 348 , trackbacks - 0

Generating a SAS token for Service Bus in .Net Core

Microsoft has decided to separate the queue/topic send/receive functionality from the queue/topic management functionality.  Some of these separations make sense, while others, like the inability to auto-provision new queues/topics does not.

In any event, we can still create these objects using the service bus REST API, but it requires some special handling, especially for authorization.

The send/receive client library uses a connection string for authentication.  This is great.  Its easy to use, and can be stored as a secret. No fuss, no muss.  The REST API endpoints require a SAS token for authorization.  You would think there would be a provider to produce a SAS token for a resource, given the resource path and connection string.  You would be wrong.  Finding working samples of the token generation using .net core 2.x was surprisingly difficult.  In any event, after (too) much researching, I’ve come up with this:

public interface ISasTokenCredentialProvider
  {
     string GenerateTokenForResourceFromConnectionString(string resourcePath, string connectionString, TimeSpan? expiresIn = null);
  }

public class SasTokenCredentialProvider:ISasTokenCredentialProvider
  {
     private readonly ILogger<SasTokenCredentialProvider> logger;

     public SasTokenCredentialProvider(ILogger<SasTokenCredentialProvider> logger)
     {
          this.logger = logger;
     }
   

     public string GenerateTokenForResourceFromConnectionString(string resourcePath, string connectionString, TimeSpan? expiresIn = null)
     {
         if (string.IsNullOrEmpty(resourcePath))
         {
             throw new ArgumentNullException(nameof(resourcePath));
         }

         if (string.IsNullOrEmpty(connectionString))
         {
              throw new ArgumentException(nameof(connectionString));
         }
        
         // parse the connection string into useful parts
         var connectionInfo = new ServiceBusConnectionStringInfo(connectionString);

         // concatinate the service bus uri and resource paths to form the full resource uri
         var fullResourceUri = new Uri(new Uri(connectionInfo.ServiceBusResourceUri), resourcePath);           
         // ensure its URL encoded
         var fullEncodedResource = HttpUtility.UrlEncode(fullResourceUri.ToString());
        
         // default to a 10 minute token
         expiresIn = expiresIn ?? TimeSpan.FromMinutes(10);
         var expiry = this.ComputeExpiry(expiresIn.Value);


         // generate the signature hash
          var signature = this.GenerateSignedHash($"{fullEncodedResource}\n{expiry}", connectionInfo.KeyValue);

      

         // assembly the token
         var keyName = connectionInfo.KeyName;
          var token = $"SharedAccessSignature sr={fullEncodedResource}&sig={signature}&se={expiry}&skn={keyName}";

         this.logger.LogDebug($"Generated SAS Token for resource: {resourcePath} service bus:{connectionInfo.ServiceBusResourceUri} token:{token}");
         return token;
     }

     private long ComputeExpiry(TimeSpan expiresIn)
     {
         return DateTimeOffset.UtcNow.Add(expiresIn).ToUnixTimeSeconds();           
     }

     private string GenerateSignedHash(string text, string signingKey)
      {
         using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey)))
         {
             return HttpUtility.UrlEncode(Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(text))));
         }
     }
  }

And here's the code for parsing the connection string:

public class ServiceBusConnectionStringInfo
  {
      private const string ServiceBusConnectionStringProtocol = "sb://";
      private const string ServiceBusUriSuffix = ".servicebus.windows.net/";
      public ServiceBusConnectionStringInfo(string connectionString)
      {
          this.ConnectionString = connectionString;
          this.ServiceBusNamespace = this.ParseNamespace(connectionString);
          this.ServiceBusResourceUri = this.ParseResourceUri(connectionString);
          this.KeyName = this.ParseKeyName(connectionString);
           this.KeyValue = this.ParseKeyValue(connectionString);
       }

      private string ParseResourceUri(string input)
      {           
          var start = input.IndexOf(ServiceBusConnectionStringProtocol, StringComparison.InvariantCultureIgnoreCase) + ServiceBusConnectionStringProtocol.Length;
          var stop = input.IndexOf(ServiceBusUriSuffix, StringComparison.InvariantCultureIgnoreCase)+ ServiceBusUriSuffix.Length;
          return $"https://{input.Substring(start, stop - start)}";
      }
       private string ParseNamespace(string input)
      {
           var start = input.IndexOf(ServiceBusConnectionStringProtocol, StringComparison.InvariantCultureIgnoreCase) + 5;
          var stop = input.IndexOf(ServiceBusUriSuffix, StringComparison.InvariantCultureIgnoreCase);
          return input.Substring(start, stop - start);
      }

      private string ParseKeyName(string input)
      {
          return this.ParsePart(input, "SharedAccessKeyName");
      }
      private string ParseKeyValue(string input)
      {
          return this.ParsePart(input, "SharedAccessKey");
      }
      private string ParsePart(string input, string partName)
      {
          var parts = input.Split(";").Select(i => i.Trim()).ToArray();
          var keyPart = parts.FirstOrDefault(i=>i.StartsWith(partName+"=", StringComparison.InvariantCultureIgnoreCase))
                        ??parts.FirstOrDefault(i => i.StartsWith(partName+" =", StringComparison.InvariantCultureIgnoreCase));
          // ReSharper disable once UseNullPropagation
          if (keyPart != null)
          {
               var start = keyPart.IndexOf("=", StringComparison.InvariantCultureIgnoreCase) + 1;
              return keyPart.Substring(start).Trim();
          }
          return null;
      }
 
       public string ServiceBusResourceUri { get; }
     
      public string ServiceBusNamespace { get; }
      public string ConnectionString { get;}
      public string KeyName { get;  }
       public string KeyValue { get;  }
  }

Generating a token is now trival:

var token = sasTokenCredentialProvider.GenerateTokenForResourceFromConnectionString("MyQueue",<my sb connection string>);

Print | posted on Wednesday, November 29, 2017 4:16 PM | Filed Under [ .NET Azure Messaging ]

Feedback

No comments posted yet.
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 

Powered by: