Lädt...


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


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

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

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>

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();
    }
}

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>
{
}

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);
    }
}

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.

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

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

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.

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: [email protected]

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)

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

...

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


📈 145.15 Punkte
🔧 Programmierung

🔧 Visual Studio for Mac + ASP.NET Core – Getting started with ASP.NET Core using eShopOnWeb


📈 34.37 Punkte
🔧 Programmierung

🔧 Using SkipToken for Paging in Asp.Net OData and Asp.Net Core OData


📈 31.44 Punkte
🔧 Programmierung

🔧 How To Improve Performance Of My ASP.NET Core Web API In 18x Times Using HybridCache In .NET 9


📈 28.19 Punkte
🔧 Programmierung

📰 .NET Core, ASP.NET Core und Entity Framework Core 2.1 sind fertig


📈 27.64 Punkte
📰 IT Nachrichten

🔧 [ASP.NET Core][EntityFramework Core] Update from .NET 6 to .NET 8


📈 27.51 Punkte
🔧 Programmierung

🔧 Upcoming SameSite Cookie Changes in ASP.NET and ASP.NET Core


📈 27.48 Punkte
🔧 Programmierung

🔧 Differences between Asp.net and Asp.net Core


📈 27.48 Punkte
🔧 Programmierung

🔧 Microsoft .NET Maze: Understand .NET Core Vs .NET Framework Vs ASP.NET


📈 27.37 Punkte
🔧 Programmierung

🎥 Learn Live - Create web apps and services with ASP.NET Core, minimal API, and .NET 6


📈 26.75 Punkte
🎥 Video | Youtube

🔧 How to secure a single REST API resource with multiple scopes using Keycloak


📈 26.25 Punkte
🔧 Programmierung

🔧 Creating a Simple ASP.NET Core Web API for Testing Purposes


📈 26.09 Punkte
🔧 Programmierung

🔧 Getting Started with ASP.NET Core API, EF Core, and PostgreSQL on Neon


📈 25.63 Punkte
🔧 Programmierung

🔧 How to secure minimal api microservices with asp.net core identity


📈 25.57 Punkte
🔧 Programmierung

🔧 .NET 9 Improvements for ASP.NET Core: Open API, Performance, and Tooling


📈 25.49 Punkte
🔧 Programmierung

🔧 C# (C Sharp) CRUD Rest API using .NET 7, ASP.NET, Entity Framework, Postgres, Docker and Docker Compose


📈 25.26 Punkte
🔧 Programmierung

🔧 ASP.NET Core Series: Performance Testing Techniques | On .NET


📈 25.17 Punkte
🔧 Programmierung

🔧 Okta vs Keycloak: Comparison and easy Okta to Keycloak migration guide


📈 25.12 Punkte
🔧 Programmierung

🔧 ASP.NET Core and Blazor updates in .NET Core 3.0 Preview 6


📈 24.71 Punkte
🔧 Programmierung

🔧 ASP.NET Core and Blazor updates in .NET Core 3.0 Preview 7


📈 24.71 Punkte
🔧 Programmierung

🔧 ASP.NET Core and Blazor updates in .NET Core 3.0 Preview 8


📈 24.71 Punkte
🔧 Programmierung

🔧 ASP.NET Core and Blazor updates in .NET Core 3.0 Preview 9


📈 24.71 Punkte
🔧 Programmierung

🔧 ASP.NET Core and Blazor updates in .NET Core 3.0 Release Candidate 1


📈 24.71 Punkte
🔧 Programmierung

🔧 ASP.NET Core and Blazor updates in .NET Core 3.0


📈 24.71 Punkte
🔧 Programmierung

📰 Announcing a Microsoft .NET Core and ASP.NET Core Bug Bounty


📈 24.71 Punkte
📰 IT Security Nachrichten

🔧 Understanding the Publisher-Subscriber Pattern in Content Distribution Systems by using Asp.Net Core API


📈 24.14 Punkte
🔧 Programmierung

🔧 Issue with User Role-Based Authorization in ASP.NET Core 5 REST API using JWT


📈 24.14 Punkte
🔧 Programmierung

🔧 IAM mit Keycloak: Meet the Maintainers beim Keycloak DevDay 2024​


📈 23.86 Punkte
🔧 Programmierung

📰 Microsoft legt Bug-Bounty-Programm für .NET Core und ASP.NET Core neu auf


📈 23.45 Punkte
📰 IT Nachrichten

matomo