Share via


Just a simple integration test for AAL and the JWT Handler

Here I’ll do my best to create a simple integration test involving the Windows Azure Authentication Library Beta on the client side and the JWT Handler (or if you prefer the full name, JSON Web Token Handler For the Microsoft .Net Framework 4.5, Developer Preview) on the service side. While I could’ve mocked the communication with Windows Azure Active Directory Preview , I decided not to do it in this post as it gives a bit of meat to the story. Rather than explaining all the bits and pieces, which is being done extensively as I write, for example here on Windows Azure Active Directory, here on JWT Handler and here about Windows Azure Authentication Library, I’ll go straight to work with a step-by-step tutorial. Bare in mind that this is neither a post on how to write effective integration tests, nor is it about TDD. Also be aware that it is based on preview libraries.

The idea here is that the integration test client mimics some sort of a server-side client application that consumes a REST API, both registered as Service Principals within the same AAD tenant. Let’s say for example that the client application is a long-running server-side process that are polling an API for changes, as described in this example (which inspired me to write this post). The API only allows access to requests that can present an OAuth2 bearer token that can be properly validated. So before the client can successfully make an API call it needs to aquire an access token from ACS, which it does by utilizing the AAL. The client then adds the aquired token to the HTTP request’s authorization header before making the call. The functionality under test here is the token validation part, or rather how we enable the token validation taking place.

So here it goes:

Create a Windows Azure AD tenant: here Get the Powershell CmdLets that we’ll need: 
64-bit: https://go.microsoft.com/fwlink/p/?linkid=236297
32-bit: https://go.microsoft.com/fwlink/p/?linkid=236298 

Start Visual Studio 2012 and create a new unit test project, targeting .NET Framework 4.5. In this tutorial I chose to use MS-Test.
You may call the solution and project whatever you like.

Add Nuget packages for
Microsoft.IdentityModel.Tokens.JWT
Microsoft.WindowsAzure.ActiveDirectory.Authentication

Add a reference to
System.IdentityModel
System.Net.Http
System.Web.Http
System.Windows.Forms (needed because we’ll make use of the AssertionCredential class (part of Windows.Azure.ActiveDirectory.Authentication)

Add an app.config file to the project and replace with the following content. Notice all the placeholder values that we will fill on the way:    

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.   <appSettings>
  4.     <add key="Audiences" value="service/localhost@PLACEHOLDER_FOR_TENANT_ID"/>
  5.     <add key="Issuers" value="00000001-0000-0000-c000-000000000000@PLACEHOLDER_FOR_TENANT_ID,https://sts.windows.net/PLACEHOLDER_FOR_TENANT_ID"/>
  6.     <add key="ClientSymmetricKey" value="PLACEHOLDER_FOR_CLIENT_SYMMETRIC_KEY"/>
  7.     <add key="ServiceSymmetricKey" value="PLACEHOLDER_FOR_SERVICE_SYMMETRIC_KEY"/>
  8.     <add key="Tenant" value="https://accounts.accesscontrol.windows.net/PLACEHOLDER_FOR_TENANT_ID"/>
  9.     <add key="ServiceRealm" value="service/localhost@PLACEHOLDER_FOR_TENANT_ID"/>
  10.     <add key="Resource" value="IntegrationTestClient/localhost@PLACEHOLDER_FOR_TENANT_ID"/>
  11.   </appSettings>
  12. </configuration>

Save and compile the project!

Switch over to Powershell or Powershell ISE, and run 

  1. Import-Module msonline -Force
  2. Import-Module msonlineextended -Force
  3. #When prompted for credentials, login with the same account you used when creating the tenant
  4. Connect-MsolService

If you successfully signed in with the tenant administrator credentials, you will have full access to your tenant from within the Powershell host.

Provision the REST service; i.e. register it as a Service Principal within your tenant.

  1.    
  2. New-MsolServicePrincipal -ServicePrincipalNames @("service/localhost") DisplayName "Service" -Usage Sign
  3.    

In the App.config file, replace PLACEHOLDER_FOR_SERVICE_SYMMETRIC_KEY with the symmetric key that was returned.


Provision the integration test client

  1.      
  2. New-MsolServicePrincipal-ServicePrincipalNames @("IntegrationTestClient/localhost") -DisplayName"Integration test client"UsageVerify
  3.      

In the app.config file, replace PLACEHOLDER_FOR_CLIENT_SYMMETRIC_KEY with the symmetric key that was returned.  

That’s it, we’re done with Powershell, so you may close it down.

It is time to look up your Tenant Id. All you need to accomplish this is to point your browser to the URL of your Tenant’s federation metadata; https://accounts.accesscontrol.windows.net/placeholder_for_your_tenant/FederationMetadata/2007-06/FederationMetadata.xml
where placeholder_for_your_tenant usually goes something like your_tenant_name.onmicrosoft.com. Now from the page returned, copy the GUID-value inside the entityID. then jump over to the App.config file in Visual Studio and replace all six occurences of PLACEHOLDER_FOR_TENANT_ID with the copied value.

From now on we turn all our attention to the project in Visual Studio.

Without further ado, here are the raw code as it is. No effort has been made on refactoring whatsoever as I leave that to the reader.

The integration test class

  1. using System;
  2. using System.Configuration;
  3. using System.Net;
  4. using System.Net.Http;
  5. using System.Net.Http.Headers;
  6. using System.Web.Http;
  7. using Microsoft.VisualStudio.TestTools.UnitTesting;
  8. using Microsoft.WindowsAzure.ActiveDirectory.Authentication;
  9.  
  10. namespace IntegrationTests
  11. {
  12.     [TestClass]
  13.     public class When_calling_the_service
  14.     {
  15.         private HttpConfiguration config;
  16.         private HttpServer server;
  17.         private HttpClient client;
  18.         private AssertionCredential assertionCredential;
  19.         private AuthenticationContext authContext;
  20.  
  21.         [TestInitialize]
  22.         public void With_a_valid_token()
  23.         {
  24.             config = new HttpConfiguration();
  25.             config.Routes.MapHttpRoute(name: "Default",
  26.                                        routeTemplate: "api/{controller}/{action}/{id}",
  27.                                        defaults: new { id = RouteParameter.Optional });
  28.  
  29.             config.MessageHandlers.Add(new JWTMessageHandler());
  30.             config.MessageHandlers.Add(new JustReturnsOkMessageHandler());
  31.             server = new HttpServer(config);
  32.             client = new HttpClient(server);
  33.             var token = AcquireToken();
  34.             client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Assertion);
  35.         }
  36.  
  37.         [TestMethod]
  38.         public void It_should_return_http_status_code_200()
  39.         {
  40.             //Always use SSL with bearer tokens!
  41.             var task = client.GetAsync("https://doesntmatterwhatgoesherereally");
  42.             task.Wait();
  43.             var response = task.Result;
  44.             Assert.AreEqual(response.StatusCode, HttpStatusCode.OK);
  45.         }
  46.  
  47.         [TestCleanup]
  48.         public void CleanUp()
  49.         {
  50.             server.Dispose();
  51.             client.Dispose();
  52.         }
  53.  
  54.         private AssertionCredential AcquireToken()
  55.         {
  56.             string serviceRealm = ConfigurationManager.AppSettings["ServiceRealm"];
  57.             string resource = ConfigurationManager.AppSettings["Resource"];
  58.             string symmetricKey = ConfigurationManager.AppSettings["ClientSymmetricKey"];
  59.  
  60.             var credential = new SymmetricKeyCredential(resource, Convert.FromBase64String(symmetricKey));
  61.             authContext = new AuthenticationContext(ConfigurationManager.AppSettings["Tenant"]);
  62.             assertionCredential = authContext.AcquireToken(serviceRealm, credential);
  63.             return assertionCredential;
  64.         }
  65.     }
  66. }

JWTMessageHandler, added to the HttpConfiguration MessageHandler collection above

  1. using System.Collections.Generic;
  2. using System.Configuration;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Net.Http;
  6. using System.Threading.Tasks;
  7.  
  8. namespace IntegrationTests
  9. {
  10.     public class JWTMessageHandler : DelegatingHandler
  11.     {
  12.         public JWTMessageHandler()
  13.         {
  14.         }
  15.  
  16.         public JWTMessageHandler(HttpMessageHandler innerHandler)
  17.             : base(innerHandler)
  18.         {
  19.         }
  20.  
  21.         protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
  22.         {
  23.             var jwtHandler = new CustomJWTHandler();
  24.             string token;
  25.             if (!request.TryGetToken(out token))
  26.             {
  27.                 HttpStatusCode statusCode = HttpStatusCode.Unauthorized;
  28.                 return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
  29.             }
  30.             try
  31.             {
  32.                 System.Threading.Thread.CurrentPrincipal = jwtHandler.ValidateToken(token);
  33.             }
  34.             catch (System.Exception)
  35.             {
  36.                 HttpStatusCode statusCode = HttpStatusCode.BadRequest;
  37.                 var httpResponseMessage = new HttpResponseMessage(statusCode);
  38.                 httpResponseMessage.ReasonPhrase = "Something went wrong validating the token!";
  39.                 return Task<HttpResponseMessage>.Factory.StartNew(() => httpResponseMessage);
  40.             }
  41.             return base.SendAsync(request, cancellationToken);
  42.         }
  43.     }
  44. }

As a precaution I would also add a message handler for making sure the call is made over HTTPS, as we are dealing with bearer tokens here.

HttpRequestMethodExtensions, just for the convenience when trying to extract the token above.

  1. using System.Collections.Generic;
  2. using System.Net.Http;
  3. using System.Linq;
  4.  
  5. namespace IntegrationTests
  6. {
  7.     public static class HttpRequestMessageExtensions
  8.     {
  9.         public static bool TryGetToken(this HttpRequestMessage @this, out string token)
  10.         {
  11.             token = null;
  12.             IEnumerable<string> authorizationHeaders;
  13.             if (!@this.Headers.TryGetValues("Authorization", out authorizationHeaders) || authorizationHeaders.Count() > 1)
  14.             {
  15.                 return false;
  16.             }
  17.             string bearer = authorizationHeaders.ElementAt(0);
  18.             token = bearer.ToLower().StartsWith("bearer ") ? bearer.Substring(7) : bearer;
  19.             return true;
  20.         }
  21.     }
  22. }

CustomJWTHandler, wraps the call to the JWTSecurityTokenHandler, that valildates the token on line 27.

  1. using System.Collections.Generic;
  2. using System.Configuration;
  3. using Microsoft.IdentityModel.Tokens.JWT;
  4. using System.ServiceModel.Security.Tokens;
  5.  
  6. namespace IntegrationTests
  7. {
  8.     public class CustomJWTHandler
  9.     {
  10.         public System.Security.Claims.ClaimsPrincipal ValidateToken(string jwt)
  11.         {
  12.             var issuers = new List<string>();
  13.             var audiences = new List<string>();
  14.             issuers.AddRange(ConfigurationManager.AppSettings["Issuers"].Split(new[] { ',' }));
  15.             audiences.AddRange(ConfigurationManager.AppSettings["Audiences"].Split(new[] { ',' }));
  16.  
  17.             var tokenValidationParameters = new TokenValidationParameters
  18.                 {
  19.                     AllowedAudiences = audiences,
  20.                     ValidIssuers = issuers,
  21.                     SigningToken = new BinarySecretSecurityToken(System.Convert.FromBase64String(ConfigurationManager.AppSettings["ServiceSymmetricKey"])),
  22.                     ValidateIssuer = true,
  23.                     ValidateNotBefore = true,
  24.                     ValidateExpiration = true,
  25.                     ValidateSignature = true
  26.                 };
  27.             return new JWTSecurityTokenHandler().ValidateToken(jwt, tokenValidationParameters);
  28.         }
  29.     }
  30. }

 

JustReturnsOkMessageHandler below is just a dummy message handler that returns OK.  As we aren’t actually hosting a real Web Api REST service in this test scenario, we would have gotten a 404 NotFound in response whenever the CustomJwtSecurityTokenHandler class succeeds to validate the JWT token. So we add this to the MessageHandler collection as well.

  1. using System.Net;
  2. using System.Net.Http;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5.  
  6. namespace IntegrationTests
  7. {
  8.     public class JustReturnsOkMessageHandler : DelegatingHandler
  9.     {
  10.         public JustReturnsOkMessageHandler()
  11.         {
  12.         }
  13.  
  14.         public JustReturnsOkMessageHandler(HttpMessageHandler innerHandler)
  15.             : base(innerHandler)
  16.         {
  17.         }
  18.  
  19.         protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
  20.                                                                CancellationToken cancellationToken)
  21.         {
  22.             return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((responseTask) =>
  23.                 {
  24.                     HttpResponseMessage response = responseTask.Result;
  25.                     response.StatusCode = HttpStatusCode.OK;
  26.                     return response;
  27.                 });
  28.         }
  29.     }
  30. }

 

Now when all the code is in place it’s time to compile and run the integration test. All you need is an Internet connection!