DEV Community

Horatiu
Horatiu

Posted on

Asp.Net Core and Keycloak testcontainer. Testing a secure Asp.Net Core Api using Keycloak Testcontainer

Asp.Net Core and Keycloak testcontainer

Testing a secure Asp.Net Core Api using Keycloak Testcontainer

logo

Solution and Projects setup

Create a new solution.

dotnet new sln -n KeycloakTestcontainer 
Enter fullscreen mode Exit fullscreen mode

Create and add a MinimalApi project to the solution.

dotnet new webapi -n KeycloakTestcontainer.Api dotnet sln add ./KeycloakTestcontainer.Api 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Create and add a xUnit test project to the solution.

dotnet new xunit -n KeycloakTestcontainer.Test dotnet sln add ./KeycloakTestcontainer.Test 
Enter fullscreen mode Exit fullscreen mode

Add reference to KeycloakTestcontainer.Api project.

dotnet add reference ../KeycloakTestcontainer.Api 
Enter fullscreen mode Exit fullscreen mode

Add package Testcontainers.Keycloak. Change the version as required.

dotnet add package Testcontainers.Keycloak --version x.x.x 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Add package dotnet add package FluentAssertions. Change the version as required.

dotnet add package FluentAssertions --version x.x.x 
Enter fullscreen mode Exit fullscreen mode

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(); 
Enter fullscreen mode Exit fullscreen mode

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(); 
Enter fullscreen mode Exit fullscreen mode

Add IApiMarker.cs interface to the root of KeycloakTestcontainer.Api project.

It will be used as entry point of the WebApplicationFactory<IApiMarker>

iapimarker

Test project setup

Add ApiFactoryFixture.cs class to the KeycloakTestcontainer.Test project.

image

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(); } } 
Enter fullscreen mode Exit fullscreen mode

Add ApiFactoryFixtureCollection.cs class. Using xUnit fixture collection, only a single Keycloak container will be created for all the tests.

image

Add the following code to it.

namespace KeycloakTestcontainer.Test; [CollectionDefinition(nameof(ApiFactoryFixtureCollection))] public class ApiFactoryFixtureCollection : ICollectionFixture<ApiFactoryFixture> { } 
Enter fullscreen mode Exit fullscreen mode

Now let's create the AuthenticateEndpointTests.cs test class.

image

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); } } 
Enter fullscreen mode Exit fullscreen mode

Keycloak container setup

Requirements: docker installed.

Pull the docker image

docker pull keycloak/keycloak:26.0 
Enter fullscreen mode Exit fullscreen mode

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.

image

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 
Enter fullscreen mode Exit fullscreen mode

Command will generate two files, certificate.pem and certificate.key. Do not forget to add .pem and .key extensions to .gitignore.

image

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.

image

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 
Enter fullscreen mode Exit fullscreen mode

Run the command to spin up the Keycloak container

docker compose -f .\docker-compose.yml up -d 
Enter fullscreen mode Exit fullscreen mode

Open browser and open the https://localhost:8443
You'll be redirected to the login page.

image

Login with username admin and password admin
Create a new realm

image

For simplicity we'll name the realm myrealm. Click Create.

image

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.

image

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.

image

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

image

Click Next.

Confirm that Direct access grants is enabled. For simplicity we'll create a public cllient.

image

Click Next.

image

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.

image

Fill in the form with the following values:

1.Name: audience

2.Type: Default

3.Toggle Display on consent screen to Off

image

Click Save.

image

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

image

Click Save

Click Clients on nav menu, select myclient.

Click Add client scope tab, select audience and click Add default.

image

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.

image

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) 
Enter fullscreen mode Exit fullscreen mode

image

Copy the file from container to Import folder

docker cp {container id):/tmp/{file name}.json ./{directory name}

image

Testing

Run the tests. Both tests should pass.

image

Top comments (0)