Configure Secure Service Fabric Using Azure Active Directory

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:

  • Test user to access Service Fabric Explorer (e.g https://<service fabric name>.<region>.cloudapp.azure.com:19080/Explorer)

    (Note: Once you successfully deployed Secure service fabric, make sure certificate (.pfx) is installed on your VM. Once you hit explorer URL in the browser, it will ask for user credential. Provide test user credentials)
  • (Client/Native app): note down app id (e.g a9a87e8f-b6a7-4c5b-a5a6-835aee7cb269)
  • Server app (Web API): note down app id (e.g e46df745-dad4-42a5-9f32-dac6e0e1255a)

Provisioning Secure Service Fabric

  • Go to Azure Portal, click on create a resource, and select Service Fabric Cluster and click on Create button.
  • Fill out all the information in Basic function:

    Azure
  • Fill in all details in Cluster Configuration:

    Azure
  • In Security section, for key vault create new or select existing one (this is used for securing service fabric with SSL certificate). You need to get SSL Certificate from CA provider which will require details like CNAME, Subject. This will be different for all environments.
  • Once you create a new certificate, it will be shown as -

    Azure
  • The below screen is shown once SSL certificate is created in the key vault.

    Azure
  • Click on Add an Azure Active Directory button and insert tenant id, client app id and server app id. (these values will be captured from newly created Azure active directory (CORP))

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

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

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

  • Once you insert all values, click on Ok. It will then show you next Summary section:

    Azure
  • Once all validation is passed click on Create button. It will take approximately 20 minutes to provision Secure Service Fabric.
  • Once it is successfully created go to Service fabric overview and click on explorer button.

    Azure

  • As you have already installed server certificate downloaded from Azure Key vault, on hitting explorer URL you will be prompted for User credential.
  • Enter username and password which are created in linked Azure Active Directory.

    Azure
  • Go to Key vault and download this certificate (in .pfx) to your machine/VM. (this will be required for installing on your local MMC console to access Service fabric explorer. Without this certificate we cannot access Service fabric explorer (e.g https://<service fabric name>.<region>.cloudapp.azure.com:19080/Explorer))

    Azure

  • Install above downloaded certificate on your machine/VM using MMC console. For that, go to Run>> type MMC>> Microsoft Management console will open.
  • Go to File>>Add/Remove Snap-In..
  • Select Certificate and click “Add>” button and select “My User account” and then click Finish.
  • This will open MMC console. To select above SSL certificate, go to Certificate>>Personal>>Certificate folder and right click certificate folder and select “Import”. Click Next with all default settings. This will install the certificate on your local.

    Azure

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

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.   
  11. //return endpoints.Select(endpoint => new ServiceInstanceListener(serviceContext => new OwinCommunicationListener(Startup.ConfigureApp, serviceContext, ServiceEventSource.Current, endpoint), endpoint));  
  12. return endpoints.Select(endpoint => new ServiceInstanceListener(serviceContext => new OwinCommunicationListener(Startup.ConfigureApp, serviceContext, ServiceEventSource.Current, endpoint, "<App Root Name>"), endpoint));  
  13. }  

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

  • Right click on Service and select Publish.

    Azure

  • Select the credentials of Azure subscription and then select Service fabric in the dropdown,

    Azure

  • Click on Publish button which will publish your application to Azure Service Fabric.
  • To ensure that the application is successfully deployed, hit app URL in the browser –

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

  • You already installed SSL certificate on your Machine/VM. Go to Run>>Type “MMC”.
  • It will open MMC console, open primary(server) certificate which you have installed from the downloaded key Vault.
  • Go to details tab and click on copy to File button.

    Azure
  • It will open Certificate Export Wizard click on Next button.
  • Select radio button Yes, export the private key and click Next.

    Azure

  • Select “Personal Information Exchange – PKCS# 12 (.PFX)” and click Next.

    Azure

  • Check password checkbox and enter some password (e.g Pa$$w0rd1234) and enter confirm password.

    Azure

  • Click Next button and path file path where user wants to create this file (e.g c:/certificate/sslcert.pfx)
  • Click on Next and then Finish.
  • Above steps will create SSL certificate with password at given location. This certificate will be required for uploading in API management to connect Secure Service fabric through SSL certificate.
Upload SSL certificate on API Management
  • Go to Azure portal and search for api management.
  • Go to Client certificate section and click Add button.
  • Upload your SSL certificate and enter your certificate password.

    Azure
  • Click on Create button. SSL certificate will be uploaded successfully.

    Azure

Add service fabric API in API Management

  • Go to api-management and click on APIs
  • Fill in all the details – Display name, Name, Web service URL, URI scheme. Product (if any).
  • URI scheme should be HTTP. Because api management url should be called on http protocol. (e.g http://<api-management-name>.azure-api.net)
  • Make sure you add HTTPS uri in web service URL, so as to connect your secure service fabric.
  • Click on create button.

    Azure
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 -

  • Go to windows PowerShell and login to your Azure account using below command

    Login-AzureRmAccount
  • It will prompt login screen. Pass user credential of your Azure account where api management is already created.
  • Run the following command to create API Management backend

    • $context = New-AzureRmApiManagementContext -resourcegroup <resource group name of api management>' -servicename '<api-mgt name>'
    • New-AzureRmApiManagementBackend -Context $context -Url '<url of secure service fabric>’ -Protocol http -SkipCertificateChainValidation $true

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

  • Go to API management and select APIs
  • Click on Design Tab and click Add Operation button
  • This will prompt Frontend screen to add Operation
  • Fill details – Name, URL (URL of your action (e.g newservice/api/values)

    Azure

  • Click on Create button which will create your Get action for this API. Similarly you can add all the actions for this service.
  • Go to all operations and click on pencil mark in the backend section:

    Azure

  • It will open Backend screen.
  • Add details:

    Azure

  • Select target as HTTP(s) endpoint, Gateway credentials should be Client cert and select Client certificate which is uploaded in previous steps.
  • Click on Save button
  • To ensure certificate is linked to your api, click on Code view on top right corner

    Azure

  • Two entries are important which will ensure setting are configured correct.

    • Authentication tag
    • Backend tag

  • For testing - Now go to postman app and hit api-management url (e.g https://<api-management name>/newservice/api)

Add following headers,

  • Ocp-Apim-Subscription-Key : < subscription key of specific product linked with your api in api management>

    e.g: Ocp-Apim-Subscription-Key: 5dhj952a88144ec08d6942f74bf5a713
  • Ocp-Apim-Trace: true
  • This will return you response of API through API management URL.

Create client and server app in Azure B2C directory

Prerequisites

  • Azure B2C directory should already be created and should have user created. If NOT, create it using this link.
Create Client app (Native app):
  • Go to Azure portal and browse your Azure B2C directory
  • Go to App registration option click on Add button

    Azure

  • Provide client app name, select Native app as ‘Yes’ –
  • Click on Create
    Azure
Create Server app (WEBAPI app)

Azure
  • Create client and server Application ids (e.g 425504fe-0b0b-40f9-bbb6-1a857dc0470b)
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.   
  9.                 WebRequest request = WebRequest.Create(requestedUrl);  
  10.                 request.Method = "POST";  
  11.                 byte[] byteArray = Encoding.UTF8.GetBytes(postData);  
  12.                 request.ContentType = "application/x-www-form-urlencoded";  
  13.                 request.ContentLength = byteArray.Length;  
  14.                 Stream dataStream = request.GetRequestStream();  
  15.                 dataStream.Write(byteArray, 0, byteArray.Length);  
  16.                 dataStream.Close();  
  17.                 WebResponse response = request.GetResponse();  
  18.                 dataStream = response.GetResponseStream();  
  19.                 StreamReader reader = new StreamReader(dataStream);  
  20.                 string responseFromServer = reader.ReadToEnd();  
  21.                 dynamic result = JsonConvert.DeserializeObject(responseFromServer);  
  22.                 this.accessToken = result.access_token; // Get Access token from client app  
  23.                 this.idToken = result.id_token;  
  24.                 this.refreshToken = result.refresh_token;  
  25.   
  26.                 SignOutButton.Visibility = Visibility.Visible;  
  27.   
  28. ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };  
  29. //// 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.  
  30. System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;  
  31.   
  32.                     HttpClient client = new HttpClient();  
  33. HttpRequestMessage request1 = new HttpRequestMessage(HttpMethod.Get, apiEndpoint); // Create Request  
  34.   
  35. request1.Headers.Add("Ocp-Apim-Subscription-Key""<subscription key of product obtained from Developer portal>"); // Add Product subscription key status  
  36. request1.Headers.Add("Host""<host key given in API gateway>"); // Add Host key (in our case this is Name of the API)  
  37.   
  38.                 // Add token to the Authorization header and make the request  
  39.                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.

  • Go to startup.cs and make changes (marked in yellow),
    1. public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];  
    2.         public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];  
    3.         public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];  
    4.         public static string SignUpSignInPolicy =   
    5. ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];  
    6.         public static string DefaultPolicy = SignUpSignInPolicy;  
    7.         // This code configures Web API. The Startup class is specified as a type  
    8.         // parameter in the WebApp.Start method.  
    9.         public static void ConfigureApp(IAppBuilder appBuilder)  
    10.         {  
    11.             // Configure Web API for self-host.   
    12.             HttpConfiguration config = new HttpConfiguration();  
    13.               
    14.   
    15.             config.Routes.MapHttpRoute(  
    16.                 name: "DefaultApi",  
    17.                 routeTemplate: "api/{controller}/{id}",  
    18.                 defaults: new { id = RouteParameter.Optional }  
    19.             );  
    20.   
    21.             TokenValidationParameters tvps = new TokenValidationParameters  
    22.             {  
    23.                 // Accept only those tokens where the audience of the token is equal to the client ID of this app  
    24.                 ValidAudience = ClientId,  
    25.                 AuthenticationType = DefaultPolicy,  
    26.   
    27.             };  
    28.   
    29.             appBuilder.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions  
    30.             {  
    31.                 AuthenticationMode = AuthenticationMode.Active,  
    32.                 // This SecurityTokenProvider fetches the Azure AD B2C metadata & signing keys from the OpenIDConnect metadata endpoint  
    33.                 AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(String.Format(AadInstance, Tenant, DefaultPolicy))),  
    34.             });  
    35.   
    36.             appBuilder.UseWebApi(config);  
    37.     config.EnableSwagger(c => c.SingleApiVersion("v1""valueService")).EnableSwaggerUi();  
    38.               
    39.         }  
  • Go to App.config and add following lines keys,
    1. <appSettings>  
    2.    <add key="ida:AadInstance" value="https://login.microsoftonline.com/{0}/v2.0/.well-known/openid-configuration?p={1}" />  
    3.     <add key="ida:Tenant" value="<B2C directory name>.onmicrosoft.com"></add>  
    4.     <add key="ida:ClientId" value="<server app id>" />  
    5.     <add key="ida:SignUpSignInPolicyId" value="<Sign in policy name>" />  
    6.     <!-- The following settings is used for requesting access tokens -->  
    7.     <add key="api:ReadScope" value="read" />  
    8.     <add key="api:WriteScope" value="write" />  
    9.     <add key="ida:Audience" value="<App URI in server app of B2C >" />  
    10.   </appSettings>  
  • Go to solution explorer and add this file under App_Start folder,

    Azure
  • Changes in API controller, Go to cs and add Authorize attribute on class,
    1. [Authorize]  
    2.   [ServiceRequestActionFilter]  
    3.   public class ValuesController : ApiController  
    4.   { 
  • It should authorize all the request coming from WPF client.

References

  • https://matt.kotsenas.com/posts/https-in-service-fabric-web-api
  • https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-cluster-security
  • https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-service-manifest-resources
  • https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-mutual-certificates-for-clients