From RFC 7519 page, JWT token is a compact, URL-safe mean of representing claims between two parties. It’s an encoded JSON object which is passed with the HTTP request to the protected HTTP API.
If you are developing a modern cloud services, you are probably using some sort of OAuth2 authentication flow to authorize users who are using your services. You could be also using Saas solutions like Auth0, Okta or Azure AD B2C. We are currently using Azure AD B2C and this post is related to that. I won’t go into more details about the providers, but I’ll explain an issue that you will hit sooner or later in your development cycle…
and that is…. testing. The question can be: “How to obtain/create a JWT token for automated integration testing of the protected HTTP API?” There are many ways of doing that, I will go with 2 methods below.
1. Use Resource Owner Password Credential flow – ROPC
ROPC grant type is suitable when resource owner (user) has a trust relationship with the client. To put it simple, use username and password to get the JWT token from identity provider. This flow should be rarely used in production scenarios, use instead some other flows like authorization flow. But for integration testing ROPC sounds useful.
Since we are using Azure AD B2C, we started our journey with this guide. In a nutshell, you need to create new native client application in Azure AD B2C tenant and ROPC policy which will be used in the HTTP request to the Token endpoint of the identity provider.
We got the JWT token but it was invalid, because It was missing the “audience” parameter which for some reason can not be set using the guide above. This looks like the limitation of the ROPC implementation on the Azure AD B2C side. Don’t get me wrong, JWT works for simple authorization scenarios, where the client application stands on it’s own, it does not rely on some HTTP API service, which needs JWT for authorization. The error we get back is The audience <guid> is invalid.
If someone has a solution for that, please let me know. 🙂
2. Use JWT mock
The above method was not useful to our case, so we went further and tried to mock the JWT. Luckily there is an updated nuget called WebMotions.Fake.Authentication.JwtBearer, which looked perfect for our scenario and give it a go! Check the nuget Github page here. The main article that talks about similar things as I am is here.
We use latest .NET Core 3.1 in our services and the WebApplicationFactory for our integration tests. And now for some code.
First we have a CustomWebApplicationFactory, which defines the startup when a test project runs. At line 9, there is a reference to a Fake JwtBearer nuget, and at the line 17 we add it to the service collection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.Reflection; using WebApi; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using WebMotions.Fake.Authentication.JwtBearer; namespace Geni.ExternalApi.InvoiceService.IntegrationTests { public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup> { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseTestServer().ConfigureTestServices(collection => { collection.AddAuthentication(FakeJwtBearerDefaults.AuthenticationScheme).AddFakeJwtBearer(); }); } } } |
Now let’s move to a sample xUnit tests. There are three tests that each calls the same API endpoint. I think the tests are pretty self explanatory, the idea is that we create a ExpandoObject and add few JWT properties to it which are used to identify the user and it’s role at the HTTP API endpoints.
By calling client.SetFakeBearerToken((object)data); we pack the ExpandoObject to JWT token which is then used in the HTTP client calling our API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
public class InvoicesControllerTests : IClassFixtureCustomWebApplicationFactory<Startup> { private readonly ITestOutputHelper _output; private readonly CustomWebApplicationFactory<Startup> _factory; public InvoicesControllerTests(CustomWebApplicationFactory<Startup> factory, ITestOutputHelper output) { _factory = factory; _output = output; } [Fact] public async Task DownloadInvoiceTestOk() { dynamic data = GetCustomerToken(); var url = "/api/v1/invoices/2134/download"; // Arrange var client = _factory.CreateClient(); client.SetFakeBearerToken((object)data); // Act var response = await client.GetAsync(url); _output.WriteLine(await response.Content.ReadAsStringAsync()); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task DownloadInvoiceTestForbidden() { dynamic data = GetAdminToken(); var url = "/api/v1/invoices/2134/download"; // Arrange var client = _factory.CreateClient(); client.SetFakeBearerToken((object)data); // Act var response = await client.GetAsync(url); _output.WriteLine(await response.Content.ReadAsStringAsync()); // Assert Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task DownloadInvoiceTestUnauthorized() { var url = "/api/v1/invoices/2134/download"; // Arrange var client = _factory.CreateClient(); // Act var response = await client.GetAsync(url); _output.WriteLine(await response.Content.ReadAsStringAsync()); // Assert Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } private static dynamic GetCustomerToken() { dynamic data = new ExpandoObject(); data.sub = "bc29cfa0-3d09-4d8c-9481-2e8c0eb1aa6c"; data.extension_UserRole = "Customer"; data.extension_UserType = "Customer"; return data; } private static dynamic GetAdminToken() { dynamic data = new ExpandoObject(); data.sub = "dca45f95-aee7-435e-83d3-3ca5f5a1af0e"; data.extension_UserRole = "Admin"; data.extension_UserType = "Employee"; return data; } } |
Pretty good!
This is a great way to quickly test the API endpoints without much infrastructure dependencies (it depends on your HTTP API).
Thanks to the author of original article Dominique St-amand and his work on the Fake JWT nuget.
Leave a Reply
You must belogged in to post a comment.