Exposing Azure Service Bus through APIM, generating a SAS Token and setting the Session Id

On one of my recent projects, a client application was required to place a message onto an Azure Service Bus by using a HTTP endpoint rather than using the Service Bus SDK and with the following constraints.

  • The client is unable to generate the Service Bus SAS token.
  • Service Bus Session Id needs to be set to the customer number found in the message to ensure ordered delivery by the consumer.
  • All  messages are to be sent to a Service Bus Topic and require to add a custom Service Bus property called ‘MgsType’.
  • Custom HTTP Request Headers may be used.

I decided upon a solution that uses Azure APIM to expose the Service Bus endpoint.  A custom policy will be used to generate the Service Bus SAS token and to parse the message for the customer Id. The customer Id will then be used to set the SessionId on the Service Bus. The client then only has to register for a subscription key in the Azure Developer portal and then pass this key with each HTTP request in the header.

image

The first step of this solution  is to create an Azure Service Bus with a Topic called ‘transactions’ and to create a Shared access policy called ‘Sender’ which has only Send claims.

image

Next add a subscription to the topic with ‘Require Session’ enabled and the following  rule “MsgType = ‘Deposits’”

image

Before we start developing the custom policies, we need to setup 3 Name Values in the APIM blade as shown below. These values are used for generating the Service Bus SAS token and are obtained from the Service Bus properties.

image

Where,

  • SB_Key – primary key for the ‘Sender’ shared access policies
  • SB_KeyName  – name of the Shared access policy
  • SB_Uri – is the topic URL which can be found by clicking on the topic name under the ‘Topics’ blade shown below.

image

Next is to create an API using the ‘Blank API’ template similar to what I have done below. Note the ‘Web service URL’ value is the base address of the Service Bus topic URL. (ie without the topic name resource location)

image

Then add an operation to the service as below. Note the URL should be the name of your topic with the resource ‘messages’ appended to the end.

image

Once the operation has been created, we can now add the custom policy to the ‘Inbound processing’ stage.

image

The APIM policy consists of several code blocks inside the inbound processing stage which are describe in detail below. To improve performance I will be caching the generated SAS token.

The  first code block looks up cache for a value. If nothing is found then the variable “cachedSasToken” is assigned to null as there is no default value specified.

<cache-lookup-value key="crmsbsas" variable-name="cachedSasToken" />

Next a control flow is used to check the variable “cachedSasToken” for null and if true then a SAS token is generated using the values stored in APIM Name-Value pairs. Once the token is  calculated then it is stored in cache and set to expire in 120 seconds. The cache is then read again to assign the variable “cachedSasToken” with the generated SAS token.

Both the signature and resource URL is required to be UrlEncoded. My first choice was to use the System.Web.UrlEncode function to encode the values. Unfortunately this function is not available in the APIM policy expressions as it only has a subset of the .Net Framework types. To work around this issue, I ended up using the System.Uri.EscapeDataString method instead.

<choose>

            <when condition="@(context.Variables.GetValueOrDefault&lt;string>("cachedSasToken") == null)">

                <cache-store-value key="crmsbsas" value="@{

                        string resourceUri = "{{SB_Uri}}";

                        string keyName = "{{SB_KeyName}}";

                        string key = "{{SB_Key}}";

                        TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);

                        var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + 120);

                        string stringToSign = System.Uri.EscapeDataString(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("SharedAccessSignature sr={0}&amp;sig={1}&amp;se={2}&amp;skn={3}", 

                                        System.Uri.EscapeDataString(resourceUri),

                                        System.Uri.EscapeDataString(signature), expiry, keyName);

                        return sasToken;

                    }" duration="100" />

                <cache-lookup-value key="crmsbsas" variable-name="cachedSasToken" />

            </when>

        </choose>

The SAS token is then added to the header using the value from the variable ‘cachedSasToken’

<set-header name="Authorization" exists-action="override">

<value>@(context.Variables.GetValueOrDefault<string>("cachedSasToken"))</value>

</set-header>

I then set the message content type to application/json and remove the APIM subscription key header from being sent to the Service Bus endpoint.

<set-header name="Content-type" exists-action="override">

          <value>application/json</value>

      </set-header>

      <set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />

The last part is to extract the customer number from the message and assign it to the SessionId property of the Service Bus. The standard set of Service Bus properties are required to be added to  a custom header called ‘BrokerProperties’

<set-header name="BrokerProperties" exists-action="override">

            <value>@{

               string reqData = context.Request.Body?.As<string>(true);

               dynamic data =  JsonConvert.DeserializeObject(reqData);

               string order =  data?.CustomerNumber;

               return string.Format("{{\"SessionId\":\"{0}\"}}", order);

            }</value>

        </set-header>

The full code for the custom policy is here:

<!--

    IMPORTANT:

    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.

    - Only the <forward-request> policy element can appear within the <backend> section element.

    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.

    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.

    - To add a policy position the cursor at the desired insertion point and click on the round button associated with the policy.

    - To remove a policy, delete the corresponding policy statement from the policy document.

    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.

    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.

    - Policies are applied in the order of their appearance, from the top down.

-->

<policies>

    <inbound>

        <base />

        <cache-lookup-value key="crmsbsas" variable-name="cachedSasToken" />

        <choose>

            <when condition="@(context.Variables.GetValueOrDefault&lt;string>("cachedSasToken") == null)">

                <cache-store-value key="crmsbsas" value="@{

                        string resourceUri = "{{SB_Uri}}";

                        string keyName = "{{SB_KeyName}}";

                        string key = "{{SB_Key}}";

                        TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);

                        var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + 120);

                        string stringToSign = System.Uri.EscapeDataString(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("SharedAccessSignature sr={0}&amp;sig={1}&amp;se={2}&amp;skn={3}", 

                                        System.Uri.EscapeDataString(resourceUri),

                                        System.Uri.EscapeDataString(signature), expiry, keyName);

                        return sasToken;

                    }" duration="10" />

                <cache-lookup-value key="crmsbsas" variable-name="cachedSasToken" />

            </when>

        </choose>

        <set-header name="Authorization" exists-action="override">

            <value>@(context.Variables.GetValueOrDefault<string>("cachedSasToken"))</value>

        </set-header>

        <set-header name="Content-type" exists-action="override">

            <value>application/json</value>

        </set-header>

        <set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />

        <set-header name="BrokerProperties" exists-action="override">

            <value>@{

               string reqData = context.Request.Body?.As<string>(true);

               dynamic data =  JsonConvert.DeserializeObject(reqData);

               string order =  data?.CustomerNumber;

               return string.Format("{{\"SessionId\":\"{0}\"}}", order);

            }</value>

        </set-header>

    </inbound>

    <backend>

        <base />

    </backend>

    <outbound>

        <base />

    </outbound>

    <on-error>

        <base />

    </on-error>

</policies>

 

Now lets use Postman to send a message to the URL endpoint exposed by APIM to test the policy. The headers contain the APIM subscription key and a custom header for the ‘MsgType’ which will be used for the Service Bus subscription filter.

image

The message body simply contains the Customer number and the amount to deposit.

image

After posting the message to the APIM endpoint URL, we can see the message was successfully forwarded to the Service Bus by using Service Bus Explorer to view the message properties and content.

Notice the message custom properties has the MsgType and the ‘SessionId’ is populated with the customer number.

image

Enjoy…

Leave a Reply

Your email address will not be published. Required fields are marked *