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

 

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

Rate This
  • Comments 12

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;
              }
         }
    }
    
    

    Copying all the code in BearerTokenMessageInspector.cs

  3. 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

  4. 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

  5. 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

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

    ex3_task2_j

    Adding references

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

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

    ex3_task2_l

    Adding a reference

  9. 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>
    
    
  10. 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)

  11. Publish the service. Click the publish button.

    ex3_task2_u

    Publishing the service

  12. 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

  • So why didn't you specify what modifications have to be made on the client now? I can not find that information anywhere. The only link I came across regarding that is dead:

    msdn.microsoft.com/.../gg185954.aspx

  • Ivan, See the next post:

    blogs.msdn.com/.../implementing-authentication-security-with-a-wpf-client-app-and-a-wcf-service-using-azure-directory-services.aspx

  • I have my WCF service setup and working. Now I am trying to get it onboarded with AAD. So I came across this article.

    I created the App_Code folder [as it was not present in WCF webrole by default] and inside that I created the BearerTokenMessageInspector.cs.

    WCFMockService is my webrole name

    MockService is my service.

    My webconfig has :

       <behaviors>

         <serviceBehaviors>

           <behavior>

             <bearerTokenRequired/>

             <serviceMetadata httpsGetEnabled="true"/>

             <serviceDebug includeExceptionDetailInFaults="true"/>

           </behavior>

          </serviceBehaviors>

       </behaviors>

       <extensions>

         <behaviorExtensions>

           <add name="bearerTokenRequired"

              type="WCFMockService.MockService.BearerTokenExtensionElement, App_Code"/>

         </behaviorExtensions>

       </extensions>

    I am getting the following error after publishing the service.

    The type 'WCFMockService.MockService.BearerTokenExtensionElement, App_Code' registered for extension 'bearerTokenRequired' could not be loaded.

    I even tried changing the extension to

     <extensions>

         <behaviorExtensions>

           <add name="bearerTokenRequired"

              type="WCFMockService.MockService.BearerTokenExtensionElement, App_Code, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>

         </behaviorExtensions>

    Still doesn't work. Please Help.

    Thanks in advance.

  • On more troubleshooting, I found that even though I have System.IdentityModel.Services and System.Net.Http referenced and showing in References list, still they are not found and marked red in BearerTokenMessageInspector.cs.

    Error:

    - "Cannot resolve symbol Services" and

    - "Cannot resolve symbol Http"

  • Resolved the above errors for references.

    But still getting the initial error.

    The type 'WCFMockService.MockService.BearerTokenExtensionElement, App_Code' registered for extension 'bearerTokenRequired' could not be loaded.

  • I provide the entire source code for client and server that ran successfully on my machine. Unfortunately, I am unable to debug code for my readers. I apologize that this not something I cannot support due to limited time.

  • I understand. No Problem.

    I am confused because my service gets created if I run the WCF service web role on local machine but when I publish to azure, it gives the error for extension not found.

  • Fixed the error on Azure as well.

    But now when I try to add reference to WCF service, I get a pop up window to enter username and password? and if I don't enter credentials, it is not allowing me to reference service.

    Which credentials to enter here? Did you got same window in your process?

  • I am getting same issue like Jason.

    I am done till making changes to service but now on client, when I am trying to reference the service, I get a window for entering credentials. I have entered various kind of credentials but nothing works.

    If I remove  <bearerTokenRequired/> from web.config then I can reference the service and it doesn't ask for credentials but that also, means the service is not behind AAD anymore.

    So, need your guidance on what should I remove from the your given code that keeps the service behind AAD but doesn't prompt credentials on referencing to the service.

    I feel the reference to service should not be restricted by Azure AD. If my understanding is not correct, then what credentials we should enter to successfully reference the service?

    Please help.

  • Hi,Jason how did u fix your above mentioned error? I am getting a similar error

  • Bruno,

    Would it be possible to update this using the latest libraries and using a windows8 store app as the client?

    Thanks.

  • Nice work Bruno.  Is there an example for a java client ?

Page 1 of 1 (12 items)
Leave a Comment
  • Please add 2 and 2 and type the answer here:
  • Post