Create Secure Service Fabric on Azure Portal

Prerequisites

Before provisioning secure service fabric make sure you already have Azure Tenant Active directory configured and it should contain the following:

Provisioning Secure Service Fabric

e.g tenantID : 28ebb319-1ef1-4724-b85b-ada7546d1d7b

clientappID : 426604fe-0b0b-40f9-bbb6-1a857dc0470b

serverappID : 89ba6268-d231-4f74-a3a1-f88996a3e8ca

Set HTTPS endpoint in Service Fabric code base

Prerequisites

Before we start, here are the prerequisites for this article. These instructions were written and tested against the following versions of the Service Fabric API, though the code and techniques likely apply to other versions as well.

Add an HTTPS endpoint to the Service and Application manifests

Make sure you have a certificate in your Service Fabric cluster

If you followed the Secure a Service Fabric cluster docs then you already have the management certificate that you can reuse for this purpose. If your cluster is insecure, then go back and fix that first!

Add the certificate to the LocalMachine\My store on your dev machine for debugging

According to the docs, the certificate should be added to CurrentUser\My, CurrentUser\TrustedPeopleand LocalMachine\My This is because the Service Fabric local cluster runs as NETWORK SERVICE and not as your user account. The PowerShell command to import the certificate is

Import-PfxCertificate -Exportable -CertStoreLocation Cert:\LocalMachine\My -FilePath C:\path\to\cert.pfx -Password (Read-Host -AsSecureString -Prompt "Enter Certificate Password")

Update ServiceManifest.xml and ApplicationManifest.xml

ServiceManifest.xml

  1. <Endpoint Protocol="http" Name="ServiceEndpoint" Type="Input" Port="8521" />
  2. <Endpoint Protocol="https" Name="ServiceEndpointHttps" Type="Input" Port="443" />
  3. </Endpoints>
  4. </Resources>
  5. </ServiceManifest>

ApplicationManifest.xml

  1. <ServiceManifestImport>
  2. <ServiceManifestRef ServiceManifestName="SampleWebApiPkg" ServiceManifestVersion="1.0.0" />
  3. <ConfigOverrides />
  4. <Policies>
  5. <EndpointBindingPolicy EndpointRef="ServiceEndpointHttps" CertificateRef="MyCert" />
  6. </Policies>
  7. </ServiceManifestImport>
  8. <DefaultServices>
  9. <Service Name="SampleWebApi">
  10. </StatelessService>
  11. </Service>
  12. </DefaultServices>
  13. <Certificates>
  14. <EndpointCertificate X509FindValue="<Thumbprint of your certificate>" Name="MyCert" />
  15. </Certificates>
  16. ;/ApplicationManifest>

Open the Load Balancer port(s)

Navigate to your Azure Load Balancer (it was created automatically in the same resource group as your Service Fabric cluster) and add two new Load balancing rules and Probes if they don't already exist (refer below table and screenshot).

Probe / LB RuleNamePort
ProbeWebApiHttpHTTP on port 8521
ProbeWebApiHttpsTCP on port 443

Azure

Update OwinCommunicationListener

Now we've got two service endpoints in our manifest. We're done, right? Unfortunately, if you look at the Diagnostic Events for your service you should see logs similar to this

TimestampEvent NameMessage
6:47:57 PMStatelessRunAsyncCompletionRunAsync has successfully completed for a stateless service instance
6:47:57 PMStatelessRunAsyncInvocationRunAsync has been invoked for a stateless service instance
6:47:57 PMServiceMessageListening on Http://10.0.0.4:8521/
6:47:56 PMServiceMessageStarting web server on Http://+:8521/
6:47:56 PMServiceTypeRegisteredService host process 4472 register service type

Interestingly, the this.listeningAddress has "http" hardcoded! We can update this code to pull the protocol from the endpoint definition in the manifest, and while we're at it, let's add some additional logging which will come in handy later. The diff should look like this:

OwinCommunicationListener.cs

  1. public Task<string> OpenAsync(CancellationToken cancellationToken)
  2. {
  3. + this.eventSource.ServiceMessage(this.serviceContext, "Calling OpenAsync on endpoint {0}", this.endpointName);
  4. var serviceEndpoint = this.serviceContext.CodePackageActivationContext.GetEndpoint(this.endpointName);
  5. var protocol = serviceEndpoint.Protocol;
  6. int port = serviceEndpoint.Port;
  7. + this.eventSource.ServiceMessage(this.serviceContext, "Found endpoint with protocol '{0}' port '{1}'", protocol, port);
  8. if (this.serviceContext is StatefulServiceContext)
  9. {
  10. StatefulServiceContext statefulServiceContext = this.serviceContext as StatefulServiceContext;
  11. this.listeningAddress = string.Format(
  12. CultureInfo.InvariantCulture,
  13. - "http://+:{0}/{1}{2}/{3}/{4}",
  14. + "{0}://+:{1}/{2}{3}/{4}/{5}",
  15. protocol,
  16. port,
  17. string.IsNullOrWhiteSpace(this.appRoot)
  18. ? string.Empty
  19. {
  20. this.listeningAddress = string.Format(
  21. CultureInfo.InvariantCulture,
  22. - "http://+:{0}/{1}",
  23. + "{0}://+:{1}/{2}",
  24. protocol,
  25. port,
  26. string.IsNullOrWhiteSpace(this.appRoot)
  27. ? string.Empty
  28. }
  29. catch (Exception ex)
  30. {
  31. - this.eventSource.ServiceMessage(this.serviceContext, "Web server failed to open. " + ex.ToString());
  32. + this.eventSource.ServiceMessage(this.serviceContext, "Web server for endpoint {0} failed to open. {1}", this.endpointName, ex.ToString());
  33. this.StopWebServer();
  34. public Task CloseAsync(CancellationToken cancellationToken)
  35. {
  36. - this.eventSource.ServiceMessage(this.serviceContext, "Closing web server");
  37. + this.eventSource.ServiceMessage(this.serviceContext, "Closing web server for endpoint {0}", this.endpointName);
  38. this.StopWebServer();
  39. public void Abort()
  40. {
  41. - this.eventSource.ServiceMessage(this.serviceContext, "Aborting web server");
  42. + this.eventSource.ServiceMessage(this.serviceContext, "Aborting web server for endpoint {0}", this.endpointName);
  43. this.StopWebServer();
  44. }

Update CreateServiceInstanceListeners()

  1. protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
  2. {
  3. //return new ServiceInstanceListener[]
  4. //{
  5. // new ServiceInstanceListener(serviceContext => new OwinCommunicationListener(Startup.ConfigureApp, serviceContext, ServiceEventSource.Current, "ServiceEndpoint"))
  6. //};
  7. var endpoints = Context.CodePackageActivationContext.GetEndpoints()
  8. .Where(endpoint => endpoint.Protocol == EndpointProtocol.Http || endpoint.Protocol == EndpointProtocol.Https)
  9. .Select(endpoint => endpoint.Name);
  10. //return endpoints.Select(endpoint => new ServiceInstanceListener(serviceContext => new OwinCommunicationListener(Startup.ConfigureApp, serviceContext, ServiceEventSource.Current, endpoint), endpoint));
  11. return endpoints.Select(endpoint => new ServiceInstanceListener(serviceContext => new OwinCommunicationListener(Startup.ConfigureApp, serviceContext, ServiceEventSource.Current, endpoint, "<App Root Name>"), endpoint));
  12. }

This creates an endpoint for each HTTP / HTTPS endpoint found in the manifest. Additionally, we name the ServiceInstanceListener the name of the endpoint since by default it has a blank name, and each listener must have a unique name.

Deploy Application on Secure Service Fabric

https://<service fabric name>.<region>.cloudapp.azure.com/<app root name>/api/values

e.g: https://secure-sf.westeurope.cloudapp.azure.com/drsservice/api/values

Configure Service Fabric in Azure API Management

Prerequisites

Assuming you have already created API Management.

Generate password protected SSL certificate for API management

Upload SSL certificate on API Management

Add service fabric API in API Management

Add API Management backend for your API using powershell

If you are using self-signed certificates, you will need to disable certificate chain validation in order for API Management to communicate with the backend system, otherwise it will return a 500 error code. To configure this, you can use the New-AzureRmApiManagementBackend (for new back end) or Set-AzureRmApiManagementBackend (for existing back end) PowerShell cmdlets and set the -SkipCertificateChainValidation parameter to True. Following steps are used to set backend -

e.g $context = New-AzureRmApiManagementContext -resourcegroup 'Horizon-RG-Dev-Env' -servicename 'dev-env-api-mgt'

New-AzureRmApiManagementBackend -Context $context -Url 'https://secure-service-fabric.westeurope.cloudapp.azure.com/drsservice' -Protocol http -SkipCertificateChainValidation $true

Link SSL certificate to service fabric API in API Management

Add following headers,

Create client and server app in Azure B2C directory

Prerequisites

Create Client app (Native app):
Create Server app (WEBAPI app)

Azure
Changes in client and server applications for jwtToken authentication

Client app(WPF) changes to acquire access token

  1. String apiEndpoint = "http://cb6deb37-b838-4591-9e31-589c71bcbcf4.cloudapp.net/drsservice/api/values"; // URL of Application Gateway or you can use API management URL.
  2. Uri myUri = new Uri(e.Uri.AbsoluteUri);
  3. string code = HttpUtility.ParseQueryString(myUri.Query).Get("code");
  4. if (!string.IsNullOrEmpty(code))
  5. {
  6. string postData = "grant_type=authorization_code&client_id=<Client id from client app created in B2C directory>&scope=https://<b2c directory name>.onmicrosoft.com/<server app name>/user_impersonation offline_access openid&code=" + code + "&redirect_uri=urn:ietf:wg:oauth:2.0:oob&session_state";
  7. string requestedUrl = "https://login.microsoftonline.com/tenantid/oauth2/v2.0/token?p=B2C_1_signin";
  8. WebRequest request = WebRequest.Create(requestedUrl);
  9. request.Method = "POST";
  10. byte[] byteArray = Encoding.UTF8.GetBytes(postData);
  11. request.ContentType = "application/x-www-form-urlencoded";
  12. request.ContentLength = byteArray.Length;
  13. Stream dataStream = request.GetRequestStream();
  14. dataStream.Write(byteArray, 0, byteArray.Length);
  15. dataStream.Close();
  16. WebResponse response = request.GetResponse();
  17. dataStream = response.GetResponseStream();
  18. StreamReader reader = new StreamReader(dataStream);
  19. string responseFromServer = reader.ReadToEnd();
  20. dynamic result = JsonConvert.DeserializeObject(responseFromServer);
  21. this.accessToken = result.access_token; // Get Access token from client app
  22. this.idToken = result.id_token;
  23. this.refreshToken = result.refresh_token;
  24. SignOutButton.Visibility = Visibility.Visible;
  25. ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
  26. //// If the server only supports higher TLS version like TLS 1.2 only, it will still fail unless your client PC is configured to use higher TLS version by default. To overcome this problem add the following in your code.
  27. System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
  28. HttpClient client = new HttpClient();
  29. HttpRequestMessage request1 = new HttpRequestMessage(HttpMethod.Get, apiEndpoint); // Create Request
  30. request1.Headers.Add("Ocp-Apim-Subscription-Key", "<subscription key of product obtained from Developer portal>"); // Add Product subscription key status
  31. request1.Headers.Add("Host", "<host key given in API gateway>"); // Add Host key (in our case this is Name of the API)
  32. // Add token to the Authorization header and make the request
  33. request1.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); /Add

Bearer Token

  1. HttpResponseMessage response1 = client.SendAsync(request1).Result;
  2. // Handle the response 200 OK

response1.StatusCode

Server app changes to access token

Before making these changes make sure you already created service fabric application using visual studio and has stateless Web API service.

References