Integration tests on protected API
If you built an api that is protected with tokens from an external IDP, you got a challenge when you want to run integration tests. They would require an access token that is signed by the IDP. And if you want to test several roles, or access levels or flows. You would need several “accounts” in your IDP. Not only that, but not all IDPs allow you to get all tokens in a non-interactive way. So you would need to have a browser open to get the tokens, which is not feasible for automated tests.
The problem
To run integration tests we would need to have tokens that are signed by the IDP, and are available in a non-interactive way. When you would create all those accounts in your IDP, you would then also need to manage all the secrets for it. Developers are not known for their love of managing secrets, especially not then they are writing tests.
Testing requirements:
- Get tokens for different roles
- Get tokens for different access levels
- Get user tokens and client tokens (client credentials flow)
- API should validate the tokens as they would be in production
- Ability to run in CI/CD pipeline
- Preferably not having to manage multiple secrets for each account
JWT Middleware
Our .net api is protected with the Microsoft.AspNetCore.Authentication.JwtBearer middleware. This package will validate tokens using the provided Authority
(which is the base uri of the IDP). Validating the tokens is done through these (simplified) steps:
- Load the open id configuration from the authority (available at
{authority}/.well-known/openid-configuration
) - Load the available keys from the authority (available at an uri in the open id configuration
jwks_uri
) - Validate the signature of the token with the public keys
- Validate the claims in the token (issuer, audience, expiration, etc).
It’s important to note that the
issuer
is validated against the issuer from the open id configuration, and not against the setAuthority
, what some libraries on other platforms do.
The solution
What if you could inject an extra signing key in the allowed signing keys? That way you would be able to sign your own tokens during tests, and you would not need to modify your application (which is important for actual integration tests).
As stated above the signing keys are loaded from the uri provided in the open id configuration (jwks_uri
), and the issuer in the token (iss
claim) is validated based on the information in the open id configuration. So if we would have a web server that loads the open id configuration from the IDP, and changes the jwks_uri
to a custom endpoint that returns the original keys and an extra key, we would be able to sign our own tokens.
Introducing the Identity Proxy
The Identity Proxy is a small web server that does exactly that. It loads the open id configuration from the IDP, and changes the jwks_uri
to a custom endpoint. This custom endpoint returns the original keys and an extra key. That extra key is a self-signed certificate that is generated the first time it’s used. There is also and endpoint to get a token, which will be signed with that certificate.
- Start the Identity Proxy
- Change the
Authority
in your api to the Identity Proxy - Get a token from the Identity Proxy
- Use the token in your integration tests
- Kill the Identity Proxy
POST http://localhost:{port}/api/identity/token
Accept: application/json
Content-Type: application/json
{
"aud": "62eb2412-f410-4e23-1111-6a91146b1111",
"sub": "99f0cbaa-b3bb-1111-1111-e8d17b221111",
"expires_in": 3600,
"additional_claim_1": "value1",
"additional_claim_2": "value2"
}
Testcontainers
Testcontainers is an open source framework for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container.
I’m a real fan of Testcontainers, it is super easy to start and dispose docker containers in yours tests. Nick Chapsas made a nice video on Testcontainers, so if you have not seen his video, sit down for a 13 minute demo on how to use Testcontainers.
Identity Proxy in Testcontainers
I’ve created a Testcontainers module for the Identity Proxy, which allows you to quickly start a proxy for that testrun and dispose it after the tests are done (or failed).
dotnet add package SvRooij.Testcontainers.IdentityProxy
As shown in the video you can use the WebApplicationFactory to setup your api to run integration tests that will actually call the endpoints. Let’s add the Identity Proxy to that factory.
public class MyApiServiceFactory: WebApplicationFactory<Startup>, IAsyncLifetime
{
private const string AUTHORITY_B2C = "https://{b2c-tenant}.b2clogin.com/{b2c-tenant}.onmicrosoft.com/B2C_1_signin_signup_acc/v2.0/";
private readonly IdentityProxyContainer _identityProxy = new IdentityProxyBuilder().WithAuthority(AUTHORITY_B2C).Build();
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
{
// Change the authority to the identity proxy
builder.UseSetting("JWT:Authority", _identityProxy.GetAuthority());
// JWT middleware does require https, unless you explicitly tell it not too.
builder.UseSetting("JWT:RequireHttpsMetadata", "false");
// Override other settings if needed
}
// Expose the token request method
internal Task<TokenResult?> GetTokenAsync(TokenRequest tokenRequest)
{
return _identityProxy.GetTokenAsync(tokenRequest);
}
// Implement the IAsyncLifetime interface to start and dispose the identity proxy
async Task InitializeAsync()
{
await _identityProxy.StartAsync();
}
async Task IAsyncLifetime.DisposeAsync()
{
await _identityProxy.DisposeAsync();
}
}
Once the WebApplicationFactory is configured you can create a test class in your Xunit
project that inherits from the IClassFixture<MyApiServiceFactory>
which means this class will be injected in the constructor and if it implements the IAsyncLifetime
interface it will be started and disposed at the right time.
public class MyControllerTests : IClassFixture<MyApiServiceFactory>
{
private readonly MyApiServiceFactory _factory;
private readonly HttpClient _client;
public MyControllerTests(MyApiServiceFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task Get_Returns200_WhenGivenMockedToken()
{
// Arrange
// Get a token from the identity proxy which runs through Testcontainers
var token = await _factory.GetTokenAsync(new TokenRequest
{
Audience = "xxx", Subject = "yyy",
// Add the same claims as you would expect from the actual IDP
AdditionalClaims = new Dictionary<string, object> {
{ "name", "Test user" },
{ "scp", "SomeScope" },
}
});
var request = new HttpRequestMessage(HttpMethod.Get, "/api/some-controller");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
// Act
var response = await _client.SendAsync(request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
// And more assertions if you want
}
}
Customer story
As a software consultant I see a lot of different setups, but most api’s I’ve seen in the last couple of years are protected with tokens from an (for the application) external token service, almost all of them support OpenID Connect. The choice for self-hosted or a hosted IdP is not that conclusive, personally I prefer a hosted IdP, because then you have someone else that worries about the security of the IdP.
Being able to test these API’s that are protected with Json Web Tokens, always was a challenge. I’ve been walking around with the idea for this IdentityProxy for a while, so I’m glad I finally found the time to build it. Even though the first version is a couple of days old, I’ve already implemented it in a big project for a customer. They were really happy with the solution, because they could now run their integration tests in their CI/CD pipeline.
After the initial setup (and building this IdentityProxy), it was really easy to create the tests for the controllers. Within a couple of hours we had a couple of dozen tests that tested the most important endpoints. And because we could easily get tokens for different roles, we could also test the authorization of the endpoints.
Code coverage
If you then combine it with this post by Tim Deschryver you can also get code coverage in your pipeline. I’m not saying that you should aim for 100% code coverage, but it’s nice to have some metrics on your tests. And if you have 100% coverage on your controllers, you can be sure that the app calling those endpoints keep working as expected.
Conclusion
Did we need yet another open source project? Yes we did! It’s important to run integration tests on your api, especially if you have mobile apps (which cannot be updated in minutes) or third party applications using your api.
Integration tests should (by definition) test the entire thing, including the authentication and authorization. And if you have a complex setup with multiple roles, scopes, and access levels, you would need to have a way to get tokens for all those scenarios. The Identity Proxy allows you to do that, without having to manage all those accounts and secrets in your IDP.
And if that is not enough of a reason, I enjoyed exploring Testcontainers, which are really handy during development and testing.
Be sure to follow me on these channels, where I regularly post various security/development related posts Twitter or LinkedIn.