Asp.Net Core and Keycloak testcontainer. Testing a secure Asp.Net Core Api using Keycloak Testcontainer
Horatiu
Posted on November 16, 2024
Asp.Net Core and Keycloak testcontainer
Testing a secure Asp.Net Core Api using Keycloak Testcontainer
Solution and Projects setup
Create a new solution.
dotnet new sln -n KeycloakTestcontainer
Create and add a MinimalApi project to the solution.
dotnet new webapi -n KeycloakTestcontainer.Api
dotnet sln add ./KeycloakTestcontainer.Api
Add package Microsoft.AspNetCore.Authentication.JwtBearer for token validation. Change the version as required.
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version x.x.x
Create and add a xUnit test project to the solution.
dotnet new xunit -n KeycloakTestcontainer.Test
dotnet sln add ./KeycloakTestcontainer.Test
Add reference to KeycloakTestcontainer.Api project.
dotnet add reference ../KeycloakTestcontainer.Api
Add package Testcontainers.Keycloak. Change the version as required.
dotnet add package Testcontainers.Keycloak --version x.x.x
Add package Microsoft.AspNetCore.Mvc.Testing. It will spin up an in memory web api for testing. Change the version as required.
dotnet add package Microsoft.AspNetCore.Mvc.Testing --version x.x.x
Add package dotnet add package FluentAssertions. Change the version as required.
dotnet add package FluentAssertions --version x.x.x
API project setup
Add Authentication and Authorization to program.cs
var builder = WebApplication.CreateBuilder(args);
š
// the realm and the client configured in the Keycloak server
var realm = "myrealm";
var client = "myclient";
builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = $"https://localhost:8443/realms/{realm}";
options.Audience = $"{client}";
});
builder.Services.AddAuthorization();
š
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
}
app.UseHttpsRedirection();
š
app.UseAuthentication();
app.UseAuthorization();
š
app.Run();
Add the secure endpoint.
var builder = WebApplication.CreateBuilder(args);
// the realm and the client configured in the Keycloak server
var realm = "myrealm";
var client = "myclient";
builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = $"https://localhost:8443/realms/{realm}";
options.Audience = $"{client}";
});
builder.Services.AddAuthorization();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
š
app.MapGet("api/authenticate", () =>
Results.Ok($"{System.Net.HttpStatusCode.OK} authenticated"))
.RequireAuthorization();
š
app.Run();
Add IApiMarker.cs
interface to the root of KeycloakTestcontainer.Api project.
It will be used as entry point of the WebApplicationFactory<IApiMarker>
Test project setup
Add ApiFactoryFixture.cs
class to the KeycloakTestcontainer.Test project.
Add the following code to ApiFactoryFixture
using DotNet.Testcontainers.Builders;
using KeycloakTestcontainer.Api;
using Microsoft.AspNetCore.Mvc.Testing;
using Testcontainers.Keycloak;
namespace KeycloakTestcontainer.Test;
public class ApiFactoryFixture : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
public string? BaseAddress { get; set; } = "https://localhost:8443";
private readonly KeycloakContainer _container = new KeycloakBuilder()
.WithImage("keycloak/keycloak:26.0")
.WithPortBinding(8443, 8443)
//map the realm configuration file import.json.
.WithResourceMapping("./Import/import.json", "/opt/keycloak/data/import")
//map the certificates
.WithResourceMapping("./Certs", "/opt/keycloak/certs")
.WithCommand("--import-realm")
.WithEnvironment("KC_HTTPS_CERTIFICATE_FILE", "/opt/keycloak/certs/certificate.pem")
.WithEnvironment("KC_HTTPS_CERTIFICATE_KEY_FILE", "/opt/keycloak/certs/certificate.key")
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(8443))
.WithClean(true)
.Build();
public async Task InitializeAsync()
{
await _container.StartAsync();
}
async Task IAsyncLifetime.DisposeAsync()
{
await _container.StopAsync();
}
}
Add ApiFactoryFixtureCollection.cs
class. Using xUnit fixture collection, only a single Keycloak container will be created for all the tests.
Add the following code to it.
namespace KeycloakTestcontainer.Test;
[CollectionDefinition(nameof(ApiFactoryFixtureCollection))]
public class ApiFactoryFixtureCollection : ICollectionFixture<ApiFactoryFixture>
{
}
Now let's create the AuthenticateEndpointTests.cs
test class.
Add the following code to it.
using FluentAssertions;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
namespace KeycloakTestcontainer.Test;
[Collection(nameof(ApiFactoryFixtureCollection))]
public class AuthenticateEndpointTests(ApiFactoryFixture apiFactory)
{
private readonly HttpClient _httpClient = apiFactory.CreateClient();
private readonly HttpClient _client = new();
private readonly string _baseAddress = apiFactory.BaseAddress ?? string.Empty;
[Fact]
public async Task AuthenticateEndpoint_WhenUserIsAuthenticated_ShouldReturnOk()
{
//Arrange
//The realm and the client configured in the Keycloak server
var realm = "myrealm";
var client = "myclient";
//Keycloak server token endpoint
var url = $"{_baseAddress}/realms/{realm}/protocol/openid-connect/token";
//Api secure endpoint
var apiUrl = "api/authenticate";
//Create the url encoded body
var data = new Dictionary<string, string>
{
{ "grant_type", "password" },
{ "client_id", $"{client}" },
{ "username", "myuser" },
{ "password", "mypassword" }
};
//Get the access token from the Keycloak server
var response = await _client.PostAsync(url, new FormUrlEncodedContent(data));
var content = await response.Content.ReadFromJsonAsync<JsonObject>();
var token = content?["access_token"]?.ToString();
//Act
//Add the access token to request header
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//Call the Api secure endpoint
var result = await _httpClient.GetAsync(apiUrl);
//Assert
result.IsSuccessStatusCode.Should().BeTrue();
result.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
}
[Fact]
public async Task AuthenticateEndpoint_WhenUserIsNotAuthenticated_ShouldReturnUnauthorized()
{
//Arrange
//The realm and the client configured in the Keycloak server
var realm = "myrealm";
var client = "myclient";
//Keycloak server token endpoint
var url = $"{_baseAddress}/realms/{realm}/protocol/openid-connect/token";
//Api secure endpoint
var apiUrl = "api/authenticate";
//Create the url encoded body
var data = new Dictionary<string, string>
{
{ "grant_type", "password" },
{ "client_id", $"{client}" },
{ "username", "myuser" },
{ "password", "badpassword" }
};
//Get the access token from the Keycloak server
var response = await _client.PostAsync(url, new FormUrlEncodedContent(data));
var content = await response.Content.ReadFromJsonAsync<JsonObject>();
var token = content?["access_token"]?.ToString();
//Act
//Add the access token to request header
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//Call the Api secure endpoint
var result = await _httpClient.GetAsync(apiUrl);
//Assert
result.IsSuccessStatusCode.Should().BeFalse();
result.StatusCode.Should().Be(System.Net.HttpStatusCode.Unauthorized);
}
}
Keycloak container setup
Requirements: docker installed.
Pull the docker image
docker pull keycloak/keycloak:26.0
To avoid the ERR_SSL_PROTOCOL_ERROR
in the browser , will use the developer certificates for https connection.
Create a Certs folder in KeycloakTestcontainer.Test. Will store the certificates here.
Open an terminal and navigate to the folder.
Create a certificate, trust it, and export it to a PEM file including the private key:
dotnet dev-certs https -ep ./certificate.crt -p $YOUR_PASSWORD$ --trust --format PEM
Command will generate two files, certificate.pem and certificate.key. Do not forget to add .pem and .key extensions to .gitignore.
Let's create a docker compose file for the initial setup of the Keycloak realm, client and users.
Add the docker-compose.yml
file to KeycloakTestcontainer.Test project.
services:
keycloak_server:
image: keycloak/keycloak:26.0
container_name: keycloak
command: start-dev --import-realm
environment:
KC_DB: postgres
KC_DB_URL_HOST: postgres_keycloak
KC_DB_URL_DATABASE: keycloak
KC_DB_USERNAME: admin
KC_DB_PASSWORD: passw0rd
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/certs/certificate.pem
KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/certs/certificate.key
ports:
- "8880:8080"
- "8443:8443"
depends_on:
postgres_keycloak:
condition: service_healthy
volumes:
- ./Certs:/opt/keycloak/certs
networks:
- keycloak_network
postgres_keycloak:
image: postgres:16.0
container_name: postgres
command: postgres -c 'max_connections=200'
restart: always
environment:
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "passw0rd"
POSTGRES_DB: "keycloak"
ports:
- "5433:5432"
volumes:
- postgres-datas:/var/lib/postgresql/data
healthcheck:
test: "exit 0"
networks:
- keycloak_network
volumes:
postgres-datas:
networks:
keycloak_network:
driver: bridge
Run the command to spin up the Keycloak container
docker compose -f .\docker-compose.yml up -d
Open browser and open the https://localhost:8443
You'll be redirected to the login page.
Login with username admin
and password admin
Create a new realm
For simplicity we'll name the realm myrealm
. Click Create.
Create a user
Initially, the realm has no users. Use these steps to create a user:
Verify that you are still in the myrealm realm, which is shown above the word Manage.
Click Users in the left-hand menu. Click Create new user.
Fill in the form with the following values:
Username: myuser
Email: myuser@email.com
First name: any first name
Last name: any last name
Click Create.
This user needs a password to log in. To set the initial password:
Click Credentials at the top of the page.
Fill in the Set password form with a mypassword
password.
Toggle Temporary to Off so that the user does not need to update this password at the first login.
Click Save.
Create Client.
Verify that you are still in the myrealm realm, which is shown above the word Manage.
Click Clients.
Click Create client
Fill in the form with the following values:
1.Client type: OpenID Connect
2.Client ID: myclient
Click Next.
Confirm that Direct access grants is enabled. For simplicity we'll create a public cllient.
Click Next.
Click Save.
By default the Client Audience is not mapped to the token. We have to create and map it.
Click on Client Scope on the left menu.
Click Create client scope tab button.
Fill in the form with the following values:
1.Name: audience
2.Type: Default
3.Toggle Display on consent
screen to Off
Click Save.
Click Mapper tab
Click Configure new mapper and select Audience
Fill in the form with the following values:
1.Name: any name
2.Included Client Audience: select myclient
Click Save
Click Clients on nav menu, select myclient
.
Click Add client scope tab, select audience and click Add default.
Export the realm configuration
In order to have this same configuration every time when the testcontainer is started, we will export this realm configuration to a import.json file. The file will be imported by the test container during start-up.
Add a folder named Import to the test project.
Open a terminal and navigate to the folder.
Identify the keyclaok container
docker ps
Access the container
docker exec -it (container id) /bin/bash
Export the realm configuration
cd /opt/keycloak/bin
./kc.sh export --file /tmp/(file name).json --realm (realm name)
Copy the file from container to Import folder
docker cp {container id):/tmp/{file name}.json ./{directory name}
Testing
Run the tests. Both tests should pass.
Posted on November 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.