Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Exercise 3: Securing a WCF service using Windows Azure Active Directory

DZone's Guide to

Exercise 3: Securing a WCF service using Windows Azure Active Directory

· Cloud Zone
Free Resource

MongoDB Atlas is a database as a service that makes it easy to deploy, manage, and scale MongoDB. So you can focus on innovation, not operations. Brought to you in partnership with MongoDB.

In this exercise, we’ll secure our WCF service using Windows Azure Active Directory.

Prerequisites

The following is required to complete this hands-on lab:

  • Microsoft Visual Studio 2013
  • The latest Windows Azure SDK for .NET
  • A Windows Azure subscription

Download the finished WPF Client and WCF Service

Previous Labs

The following is required to complete this hands-on lab:

Task 1: Creating a Windows Azure Active Directory tenant and add a user

In this task, we’ll create a Windows Azure Active Directory tenant. We’ll also add a user, which is specific to this lab. In a real environment, we would set up Active Directory synchronization so that our Azure AD instance relied on the users pushed from the on-premises instance.

  1. Log into your Azure administrative account at https://manage.windowsazure.com if not already open.

  2. In the bottom right corner, click the New button.

    newad

    Adding Azure Directory Services

  3. You will create a new active directory in the Windows Azure portal. From the portal menu, choose App Services, active directory, directory.

    ex2task3_a

    Creating a new active directory

  4. You will select custom create.

    ex2task3_b

    Creating a new active directory

  5. You will specify the new directory details. Type in name, domain name, and a country or region.

    ex2task3_c

    Specifying directory details

  6. You will validate the newly created directory. Verify the newly created directory is visible.

    ex2task3_d

    Validating the creation of a new active directory

  7. You will now add users to the new active directory. From the menu choose users.

    ex2task3_e

    Adding new users

  8. You will now add a new user. From the bottom menu bar choose add user.

    ex2task3_f

    Adding a new user

  9. You will enter user details. Choose new user for the type of user. Enter a user name.

    ex2task3_g

    Entering user details

  10. You will complete the entering of user information. Specify the first, last, and display name. This will be an ordinary user so from them role drop-down choose user.

    ex2task3_h

    Specifying user details

  11. You will now receive a temporary password. Click the create button.

    ex2task3_i

    Creating a temporary password

  12. You will receive a random password. It will also type in an email address which will contain the generated password.

    ex2task3_j

    Creating a temporary password

Task 2: Creating a Windows Azure Active Directory service application

  1. You will now associate an application with the directory services you just created. From your directory services detail pane choose applications then add an app.

    ex3_task2_a

    Adding an application for your directory services

  2. From the menu choose add an application my organization is developing.

    ex3_task2_b

    Adding an organizational application

  3. Provide a name for your application. Verify that the type is a web application or web API application.

    ex3_task2_c

    Naming your application

  4. You will verify your application properties. Note the sign-on URL as well as the app ID URI.

    ex3_task2_d

    Validating application properties

  5. You will enable users to sign into your application. Choose enable users to sign on.

    ex3_task2_e

    Enabling sign-on for users

  6. You will copy the app ID URI to the clipboard. Also take note of the Federation meta data document URL.

    ex3_task2_f

    Collecting application information from the portal

Task 2: Modifying the WCF Service

You will return back to the WCF service created in the previous steps. Start Visual Studio as administrator and open the project. The download link is provided at the beginning of this post.

  1. You will now add a class to help manage the tokens in your application. Right mouse click on App_Code, then add, followed by class. Name the class module BearerTokenMessageInspector.cs. Also note the code snippet below.

    ex3_task2_g

    Adding a new class

  2. Open the class module just created. Paste in the code from the code snippet.

    ex3_task2_g2

    Pasting in the code for BearerTokenMessageInspector.cs

    (Code Snippet - BearerTokenMessageInspector.cs)

    C#
// BearerTokenMessageInspector.cs 
// Windows Azure Active Directory Helper
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Services;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Security;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml;

namespace Expenses.WcfService
{

     public class BearerTokenMessageInspector : IDispatchMessageInspector
     {
          // You need to use what you entered
          // at the portal. You audience and authority
          // will be different.
          const string audience = "http://brunoexpenseswcf.azurewebsites.net";
          const string authority = "https://login.windows.net/expensesdomain.onmicrosoft.com";

          static string _issuer = string.Empty;
          static List<X509SecurityToken> _signingTokens = null;
          static DateTime _stsMetadataRetrievalTime = DateTime.MinValue;
          static string scopeClaimType = "http://schemas.microsoft.com/identity/claims/scope";

          public BearerTokenMessageInspector()
          {
          }

          public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
          {
                object correlationState = null;

                HttpRequestMessageProperty requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
                if (request == null)
                {
                     throw new InvalidOperationException("Invalid request type.");
                }
                string authHeader = requestMessage.Headers["Authorization"];

                if (string.IsNullOrEmpty(authHeader) || !this.Authenticate(authHeader))
                {
                     WcfErrorResponseData error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Bearer authorization_uri=\"" + authority + "\"" + "," + "resource_id=" + audience));
                     correlationState = error;
                }

                return correlationState;
          }

          private bool Authenticate(string authHeader)
          {
                const string bearer = "Bearer ";
                if (!authHeader.StartsWith(bearer, StringComparison.InvariantCultureIgnoreCase)) { return false; }

                string jwtToken = authHeader.Substring(bearer.Length);
                string issuer;
                string stsMetadataAddress = string.Format("{0}/federationmetadata/2007-06/federationmetadata.xml", authority);
                List<X509SecurityToken> signingTokens;

                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(stsMetadataAddress, out issuer, out signingTokens);

                JwtSecurityTokenHandler tokenHandler =
                     new JwtSecurityTokenHandler()
                     {
                          // For demo purposes certificate validation is turned off. Please note that this shouldn't be done in production code.
                          CertificateValidator = X509CertificateValidator.None
                     };

                TokenValidationParameters validationParameters =
                     new TokenValidationParameters()
                     {
                          //AllowedAudience = audience,
                          ValidIssuer = issuer
                          //SigningTokens = signingTokens,
                     };

                // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters);

                // Set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // Set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                     HttpContext.Current.User = claimsPrincipal;
                }

                // if the token is scoped, verify that required permission is set in the scope claim
                if ((ClaimsPrincipal.Current.FindFirst(scopeClaimType) != null) && (ClaimsPrincipal.Current.FindFirst(scopeClaimType).Value != "user_impersonation"))
                {
                     return false;
                }

                return true;
          }

          /// <summary>
          /// Parses the federation metadata document and gets issuer Name and Signing Certificates
          /// </summary>
          /// <param name="metadataAddress">URL of the Federation Metadata document</param>
          /// <param name="issuer">Issuer Name</param>
          /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
          static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
          {
                signingTokens = new List<X509SecurityToken>();

                // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
                if ((DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24)
                     || string.IsNullOrEmpty(_issuer)
                     || _signingTokens == null)
                {
                     MetadataSerializer serializer = new MetadataSerializer()
                     {
                          // turning off certificate validation for demo. Don't use this in production code.
                          CertificateValidationMode = X509CertificateValidationMode.None
                     };
                     MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                     EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                     // get the issuer name
                     if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                     {
                          _issuer = entityDescriptor.EntityId.Id;
                     }

                     // get the signing certs
                     _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                     _stsMetadataRetrievalTime = DateTime.UtcNow;
                }

                issuer = _issuer;
                signingTokens = _signingTokens;
          }

          static List<X509SecurityToken> ReadSigningCertsFromMetadata(EntityDescriptor entityDescriptor)
          {
                List<X509SecurityToken> stsSigningTokens = new List<X509SecurityToken>();

                SecurityTokenServiceDescriptor stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();

                if (stsd != null)
                {
                     IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
                                                                                 Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());

                     stsSigningTokens.AddRange(x509DataClauses.Select(token => new X509SecurityToken(new X509Certificate2(token.GetX509RawData()))));
                }
                else
                {
                     throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
                }

                return stsSigningTokens;
          }

          public void BeforeSendReply(ref Message reply, object correlationState)
          {
                WcfErrorResponseData error = correlationState as WcfErrorResponseData;
                if (error != null)
                {
                     HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty();
                     reply.Properties["httpResponse"] = responseProperty;
                     responseProperty.StatusCode = error.StatusCode;

                     IList<KeyValuePair<string, string>> headers = error.Headers;
                     if (headers != null)
                     {
                          for (int i = 0; i < headers.Count; i++)
                          {
                                responseProperty.Headers.Add(headers[i].Key, headers[i].Value);
                          }
                     }
                }
          }
     }

     public class BearerTokenServiceBehavior : IServiceBehavior
     {
          public BearerTokenServiceBehavior()
          {

          }

          public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
          {
                // no-op
          }

          public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
          {
                foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers)
                {
                     foreach (EndpointDispatcher epDisp in chDisp.Endpoints)
                     {
                          epDisp.DispatchRuntime.MessageInspectors.Add(new BearerTokenMessageInspector());
                     }
                }
          }

          public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
          {
                // no-op
          }
     }

     public class BearerTokenExtensionElement : BehaviorExtensionElement
     {
          public override Type BehaviorType
          {
                get { return typeof(BearerTokenServiceBehavior); }
          }

          protected override object CreateBehavior()
          {
                return new BearerTokenServiceBehavior();
          }
     }

     internal class WcfErrorResponseData
     {
          public WcfErrorResponseData(HttpStatusCode status) :
                this(status, string.Empty, new KeyValuePair<string, string>[0])
          {
          }
          public WcfErrorResponseData(HttpStatusCode status, string body) :
                this(status, body, new KeyValuePair<string, string>[0])
          {
          }
          public WcfErrorResponseData(HttpStatusCode status, string body, params KeyValuePair<string, string>[] headers)
          {
                StatusCode = status;
                Body = body;
                Headers = headers;
          }

          public HttpStatusCode StatusCode
          {
                private set;
                get;
          }

          public string Body
          {
                private set;
                get;
          }

          public IList<KeyValuePair<string, string>> Headers
          {
                private set;
                get;
          }
     }
}
  1. Copying all the code in BearerTokenMessageInspector.cs

  2. You will modify the audience and authority strings to reflect the information from the portal. Your URLs will differ.

    ex3_task2_q

    Modifying BearerTokenMessageInspector.cs with information from the portal

  3. You will now enable Javascript web tokens. From the tools menu choose library package manager, then package manager console.

    ex3_task2_h

    Starting package manager console

  4. Type in the following command into package manager console. Take special note that this is version 1.

    PowerShell
    Install-Package System.IdentityModel.Tokens.Jwt -version 1.0.0
    

    ex3_task2_i

    Installing the identity model package for JavaScript web tokens version 1

  5. You will need to add references. Right mouse click on the references and choose add reference.

    ex3_task2_j

    Adding references

  6. You will add two references. The first reference is seen below. Make sure it is checked. Click OK to continue.

    ex3_task2_k

    Adding a reference

  7. You will now add a second reference as seen below.

    ex3_task2_l

    Adding a reference

  8. Open the web.config file and add the following entries. Notice that we have both behavior and behavior extensions.

    ex3_task2_r

    Modifying web.config to implement authentication

    (Code Snippet - Web.config in Expeneses.WCFServcie)

    XML
<?xml version="1.0"?>
<configuration>
  <connectionStrings>
     <add
        name="Expenses.WcfService.ServiceCore.Properties.Settings.ExpensesConnectionString"
        connectionString="Server=tcp:uxpwgfs4g6.database.windows.net,1433;Database=Expenses;User ID=azureuser@uxpwgfs4g6;Password=MyP@ssw0rd;       Trusted_Connection=False;Encrypt=True;Connection Timeout=30;"
        providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
     <add
        key="aspnet:UseTaskFriendlySynchronizationContext"
        value="true"/>
  </appSettings>
  <system.web>
     <customErrors mode="Off"/>
     <compilation
        debug="true"
        targetFramework="4.5">
        <assemblies>
          <add assembly="System.Security, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/>
          <add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
          <add assembly="System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
          <add assembly="System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/>
        </assemblies>
     </compilation>
     <httpRuntime targetFramework="4.5"/>
     <machineKey
        validationKey="CCEC5BEF2C9E6574ED738A904FE0FC850604825FD09AB306C519F8C6F059A33228B7C49E69E70020C039E445DD182676FE32126299556B0E08A82E3F9E63AE8D"
        decryptionKey="517651331ADEA522A55ACD35F468DD26C2D3E52FA52EC70B0074983FBAC35A66"
        validation="SHA1"
        decryption="AES"/>
  </system.web>
  <system.serviceModel>
     <behaviors>
        <serviceBehaviors>
          <behavior>

             <!-- Add this line below. Ignore errors ->
             <bearerTokenRequired/>

             <!--To avoid disclosing metadata information, set the values below to false before deployment -->
             <serviceMetadata
                httpGetEnabled="true"
                httpsGetEnabled="true"/>
             <!--To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
             <serviceDebug includeExceptionDetailInFaults="true"/>
          </behavior>
        </serviceBehaviors>
     </behaviors>
      <!-- Add this extensions section below. ->

     <extensions>
        <behaviorExtensions>
          <add
             name="bearerTokenRequired"
             type="Expenses.WcfService.BearerTokenExtensionElement, App_Code"/>
        </behaviorExtensions>
     </extensions>

     <protocolMapping>
        <add
          binding="basicHttpsBinding"
          scheme="https"/>
     </protocolMapping>
     <serviceHostingEnvironment
        aspNetCompatibilityEnabled="true"
        multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>
  <system.webServer>
     <modules runAllManagedModulesForAllRequests="true"/>
     <!--To browse web app root directory during debugging, set the value below to true.
     Set to false before deployment to avoid disclosing web app folder information.
     -->
     <directoryBrowse enabled="true"/>
  </system.webServer>
</configuration>
  1. You are now ready to publish the updated WCF Service. Right mouse click as seen below and choose publish website.

    ex3_task2_t

    Publishing the website (the WCF service)

  2. Publish the service. Click the publish button.

    ex3_task2_u

    Publishing the service

  3. View the output window to verify correct publishing.

    ex3_task2_v

    Verifying the publish

Task 3: Running the WPF client to test the service

You will return back to the WPF client app from previous steps. Start Visual Studio as administrator and open the project. The download link for this project is provided at the beginning of this post.

  1. Return back to the WPF application (client app) to verify that the service is no longer available without a proper login and authentication.

    ex3_task2_w

    Testing the WCF service by running the WPF application

  2. You will notice that now we have a runtime error for the WPF application. We will need to make more modifications to avoid the failed attempt to connect to the WCF service.

    ex3_task2_x

    Running into runtime errors

Summary

Are many things going on with this lab.

  • You provisioned a new directory services from the Azure portal

  • You added a user to the directory services

  • You associated the WCF service with the directory services and the portal

  • You added some authentication code and the WCF service

  • You re-published the service back up to Azure websites

  • You re-tested the WPF client to verify it could not access the services because it lacks proper authentication

MongoDB Atlas is the best way to run MongoDB on AWS — highly secure by default, highly available, and fully elastic. Get started free. Brought to you in partnership with MongoDB.

Topics:

Published at DZone with permission of Bruno Terkaly, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}