How to Expose a Firewall-Protected Web Service to the Cloud with Azure Relay
Last updated on October 16, 2021
Organizations are moving more and more of their assets from on-premises to the cloud. The process can go in phases, one system at a time, and sometimes some systems are intentionally kept on on-premises servers. But what happens, when you have integrations between these systems, some of them are moved to the cloud, and some are left on firewall-protected servers?
Luckily, there are several options for us to enable our cloud applications to communicate with on-premises services:
- WCF connections offered by Azure Relay
- Hybrid connections offered by Azure Relay
- On-premises data gateway
Which one you should choose depends on what kind of an application you have in the cloud, and what kind of a service you are connecting to on-premises.
Let’s say that we have a custom WCF web service that is hosted on an old firewall-protected SharePoint Server, and we need to use the data from that web service in our cloud application. Unfortunately, our cloud solution can’t communicate directly with the SharePoint-hosted web service because of the firewall, and we are not permitted to make changes to the firewall configuration. In this blog post, I’ll show you how we can expose that single WCF web service endpoint to the cloud via Azure Relay WCF connections.
Table of Contents
- The Plan
- Setting Up the Azure Relay
- Creating the Console Application
- Setting Up the Console Application on a Server
- Authorizing the Client Application to Access the Relay
The Plan
First of all, we don’t need to make any changes to our existing web service – in this example, the one hosted by SharePoint – we can leave it as is. The only thing we need to code is a console application, which we will run at all times on a server that has access to the web service (e.g., SharePoint web-front-end server).
While running, the console application listens to the Azure Relay that we’ll set up. When a request arrives in the relay, our console application hears it, passes it on to the SharePoint hosted web service, and finally conveys the response back to the relay. Sounds simple enough? 😉 Let’s get to work!
This incredible piece of art is brought to you by Paint 3D, Surface Book and Pen!
Setting Up the Azure Relay
In Azure Portal, create a new Relay type of resource. Note down the name before the .servicebus.windows.net bit. This is your Azure Relay service namespace and you will need it later. I’m calling mine “catsarecute” after my imaginary organization.
After the resource has been created:
- Navigate to its settings.
- Open the WCF Relays section.
- Create a new WCF relay.
- Name it loosely after your web service.
- Select Http as its type.
You also need to decide whether you want to allow anonymous access to your web service, or if the calling application needs to be authorized to access the relay before their request is passed on to the web service. In this example, I don’t want to allow anonymous access, but if you do, just uncheck the Requires Client Authorization checkbox.
Creating the Console Application
Our console application will be listening to the WCF relay for requests. When a request arrives in the relay, it will hear it, and after that, it is its job to convey the message to the actual web service.
To get started:
- Create a new Console Application (.NET Framework) in Visual Studio.
- Install the WindowsAzure.ServiceBus NuGet package to the project.
- Add a Services folder to your project and a new WCF Service type of item in it. Name the item loosely after your web service. Two files will get added to the folder: an interface and a class.
Conveying the Requests to the Web Service
In the service interface, add definitions for all of your web service operations. Think about what kind of requests is the cloud application trying to make to your web service? Or, what kind of requests your web service is expecting (this should be the same thing)? Format the operation contracts to match that.
In this example, I have a web service that has only one operation. The operation returns statistics about cats between the two dates that are given as parameters in the URL. It expects a GET request and returns a response in JSON format. The URL can be, e.g., https://catsarecute.com/_vti_bin/CatStatistics.svc/GetCatStatistics/2018-09-09/2018-09-29.
using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Web; namespace CatsAreCute.Azure.ServiceBus.Relay.Services { [ServiceContract(Name = "CatStatistics", Namespace = "CatsAreCute.Azure.ServiceBus.Relay")] public interface ICatStatistics { [OperationContract, WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json, UriTemplate = "GetCatStatistics/{start}/{end}")] Message GetCatStatistics(string start, string end); } }
In the service class, implement the interface members by making new similar requests to the actual web service.
In this example, I first get the base URL for my web service from the console application’s App.config file. After that, I make the call to the web service operation using a HttpClient object. The UseDefaultCredentials=true authenticates us to the web service using the credentials that are running the console application. The response is converted to Message type and it will get conveyed back to the WCF relay, and from there to the calling application.
using System.Configuration; using System.ServiceModel; using System.ServiceModel.Channels; namespace CatsAreCute.Azure.ServiceBus.Relay.Services { [ServiceBehavior(IncludeExceptionDetailInFaults = true, Name = "CatStatistics", Namespace = "CatsAreCute.Azure.ServiceBus.Relay")] public class CatStatistics: ICatStatistics { public Message GetCatStatistics(string start, string end) { var url = $"{ConfigurationManager.AppSettings["CatStatisticsServiceUrl"]}/GetCatStatistics/{start}/{end}"; var handler = new HttpClientHandler { UseDefaultCredentials = true }; using (var httpClient = new HttpClient(handler)) { try { var response = httpClient.GetStringAsync(url).Result; return WebOperationContext.Current?.CreateJsonResponse(response); } catch (Exception) { return WebOperationContext.Current?.CreateJsonResponse(string.Empty); } } } } }
Listening to the Azure WCF Relay
Now only one more thing remains when it comes to implementing our console application. How do we get the service endpoint we just set up to actually listen to the WCF relay? It is in fact very simple.
We need to create and start a service host, which hosts our endpoint and makes it listen to the relay. You can do that in the Main method in your console application’s Program.cs file.
When creating the service host, the address points to our WCF relay, and we define the service endpoint we just created as its type. Then we just start the host, and as long as the host is running, it listens to the relay. The Console.ReadLine ensures, that the console application stays in running state until you press Enter or the process is otherwise terminated.
using System; using System.Configuration; using System.ServiceModel.Web; using CatsAreCute.Azure.ServiceBus.Relay.Services; using Microsoft.ServiceBus; namespace CatsAreCute.Azure.ServiceBus.Relay { class Program { static void Main(string[] args) { var relayServiceUri = ServiceBusEnvironment.CreateServiceUri("https", ConfigurationManager.AppSettings["ServiceNamespace"], ConfigurationManager.AppSettings["ServicePath"]); var host = new WebServiceHost(typeof(CatStatistics), relayServiceUri); host.Open(); Console.WriteLine("Listening. Press [Enter] to exit"); Console.ReadLine(); host.Close(); } } }
I’m getting the parameters for the service URL from the appSettings section of the console application’s App.config file. The service namespace is the bit before .servicebus.windows.net in your Azure Relay URL. Service path again is the name of the WFC relay.
Setting Up the Console Application on a Server
Now that you are done with your console application, build your project in Release mode. After that, copy the files located under bin/Release folder to a folder on the server that is behind the firewall and has access to the web service (e.g. SharePoint web-front-end server).
For running the console application at all times, we use Task Scheduler. Create a new task in Task Scheduler, and set your console application to start on system startup, using credentials that have access to the web service. Select the Run whether the user is logged on or not radio button and uncheck the Stop if the task runs longer than checkbox.
After the scheduled task has been created, right click on it and select Run. Now go to check the WCF relay in Azure; you should see that it has one active listener.
Now the calling application is able to make a request to the web service by using the following URL: https://catsarecute.servicebus.windows.net/catstatistics/GetCatStatistics/2018-09-09/2018-09-29
Authorizing the Client Application to Access the Relay
You were given an option to allow anonymous access when you created your WCF relay. If you did that, you don’t need to do anything else. However, if you wanted to limit who can access the web service, your calling application now needs to be authorized to access the relay.
Getting authorized to use an Azure Relay happens by including a Shared Access Signature (SAS) in the request’s Authorization header. For forming the SAS, the client application needs information from the WCF relay we set up.
Collecting the Required Information
Go to Azure Portal and…
- Click on the WCF relay you created earlier (mine was called “catstatistics”).
- Click on Shared Access Policies.
- Create a new policy by clicking Add.
- Write down the name you are giving to your policy as the keyName — you’ll need it later.
In addition to giving the policy a name, we need to define what permissions we want to grant to the calling application. In this case, we want to allow Send and Listen permissions, because we want the client application to be able to make a request to the web service and also to receive the response.
After the policy has been created, click on it. Now you can see primary and secondary keys and connection strings. Note down the primary key value as the keyValue. The secondary key is not needed at the moment, but if you at some point want to regenerate the primary key, you can give the secondary key to the calling application. Then you can regenerate the primary key without causing any downtime. It is recommended to regenerate the keys every now and then, and you can also do it if you discover that your key has been stored in an unsecured place.
The last piece of information you need is the Azure Relay namespace URL. In my case it is https://catsarecute.servicebus.windows.net. That is the resourceUrl.
Now you should have the following information which you can give to the client application, so it can create a SAS token to gain access to the WCF relay, and through that to the web service.
- Shared access policy name (keyName)
- Shared access policy primary key (keyValue)
- Azure Relay namespace URL (resourceUrl)
Creating the SAS Token
There is a very specific way of how the SAS should be formed. Here is an example of how you can do it in C#. You can find code samples for other languages here.
public static string GetSharedAccessSignature(string resourceUrl, string keyName, string keyValue) { const int validDuration = 60 * 60 * 2; var timeSinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1); var expiryTime = Convert.ToString((int)timeSinceEpoch.TotalSeconds + validDuration); var signatureString = HttpUtility.UrlEncode(resourceUrl) + "\n" + expiryTime; var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(keyValue)); var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureString))); return string.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUrl), HttpUtility.UrlEncode(signature), expiryTime, keyName); }
First, we specify, how many seconds we want the SAS to be valid for. In this case, the SAS is set to be valid for two hours. The expiry time is expressed as seconds passed since the epoch (the first of January, 1970).
Then we need to form the actual signature part of SAS. For that, we need the signature string which is a combination of the requested resource URL (the Azure Relay namespace URL) and the SAS expiry time we just constructed, separated by a newline. The signature is the SHA-256 hash computation of the signature string bytes, converted to a base64 string.
Finally, we’ll combine the following information to form the SAS token:
- The Azure Relay namespace URL (encoded)
- The signature we just created (encoded)
- The expiry time we used when creating the signature
- The key name, a.k.a the name of the WCF relay shared access policy, which primary key value we used for hashing the signature string.
The result is a SAS token and it has the following format. Now the client application just needs to include it in the Authorization header of the request.
SharedAccessSignature sr={azure-relay-namespace-url}&sig={hashed-signature}&se={expiry-time}&skn={key-name}
When Azure Relay receives a request with the SAS in its header, it will recompute the resource URL and the expiry time in the SAS using the key value (from its configuration) of the key mentioned in SAS. If the recomputed signature matches the signature in SAS, the request is authorized.
Ok, that’s everything! I hope this article was useful to you and you got your relay working. If you are interested in learning more Office 365 and Azure related things from a developer’s perspective, make sure to follow me on Twitter. I’ll let you know when I publish new content, and also if there’s something I feel is important for my fellow developers — such as you! — to be aware of.
A big thank you for reading my blog and until next time!
Laura
Thanks Laura. Yes, I’m using the HTTP action in Logic Apps to call a Web API I created which then calls the WCF Relay. This doesn’t seem right. Am I able to call the WCF Relay directly from Logic App? How would I do that?
Hi, very helpful. I have the client and host working on my machine via the Relay. However, how would I send requests to the Relay via Logic Apps, or am I approaching this the wrong way? I want to call an on-prem WCF service from a Logic App…
Hi Chris,
Instead of using HttpClient in C# code, you need to use the HTTP action in Logic Apps.
Laura