Introduction
Keycloak is a free, open-source identity and access management solution, sponsored by Red Hat, that simplifies securing modern applications with features like Single Sign-On (SSO) and identity brokering. Its centralized authentication and authorization capabilities, along with user federation and multi-tenancy support, make it a versatile tool for managing user access across various platforms. In this article, I'll guide you through configuring a .NET API with Keycloak, all running on Docker, to create a secure and scalable environment.
For simpler application cases, exploring .NET's built-in authorization and authentication mechanisms using .NET Identity, as covered in some of my previous articles, might be sufficient and worth considering. These foundational approaches can provide the necessary security for many applications without the need for more complex solutions like Keycloak. However, as your application's requirements grow, integrating Keycloak can offer enhanced flexibility and scalability.
This article contains the following:
- Initial project setup
- Docker compose file
- Keycloack server configuration
- Securing .NET API with Keycloack
For this tutorial I am using .NET 9, Keycloak version 26.1.0 and Postgres version 17.4.
Initial project setup
For the initial setup, all you need to do is to create a regular .NET API application with the default WeatherForecastController
and with the default Dockerfile
. Your Dockerfile
should look something like this:
# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 80
EXPOSE 443
# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ReactApp.Server.csproj", "."]
RUN dotnet restore "./ReactApp.Server.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./ReactApp.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./ReactApp.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ReactApp.Server.dll"]
Docker compose file
Now that you have the project setup, you need to add the following docker compose
file:
version: '3.9'
services:
backend:
build:
context: ReactApp.Server
dockerfile: Dockerfile
ports:
- "5080:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_HTTP_PORTS=80
depends_on:
- keycloak
networks:
- app-network
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
depends_on:
- postgres
networks:
- app-network
postgres:
image: postgres:17.4
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data_keycloack:/var/lib/postgresql/data
networks:
- app-network
volumes:
postgres_data_keycloack:
networks:
app-network:
driver: bridge
This Docker file is self-explanatory. It has configuration for three services: the backend service on port 5080
, the Keycloak service on port 8080
, and the PostgreSQL database for storing Keycloak’s data on port 5432
. All three services use the app-network
for communication, and the database uses a volume for persistent data storage.
To run this configuration, use the following command:
docker compose up --build
If everything is set up correctly, you should be able to navigate to http://localhost:8080/
and see the login page for Keycloak’s management interface.
You can log in using the username
and password
defined in the Docker Compose file. In this case, this is admin
for both the username and password.
Keycloack server configuration
You’ll notice that the Keycloak management console offers many settings that can be adjusted based on your use case. However, to keep this tutorial concise, I will focus only on the basic configuration.
Step 1 - Create a realm
Once you log in, you need to create a realm. A realm is a security domain that manages a set of users, credentials, roles, and groups, providing isolation between different applications or services. In my case, I am using the default realm — master
.
Step 2 - Create a client
In order for an application to be able to use Keycloaks's services such as authorization, authentication and many more, it first needs to be registers inside the Keycloak service. This can be done by creating a new client.
Go to Clients the click on Create client and add the following settings:
Client authentication must be enabled since this service is private. For public services, it should be disabled. In this case, use the Standard flow
for authentication — the flow typically used for web applications. The other flows serve different use cases. If you're interested in the specific use cases for each flow, I've included a table with brief explanations in the appendix at the end of this article.
Since this is a backend application, there’s no need to add anything in the Login Settings
section.
Securing .NET API with Keycloack
Step 1 - Add Keycloack configuration
You first need the to install the following nuget pacakge:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
In the Program.cs
, add the following configuration (note that keycloak:8080
is used to identify the service, as it runs with Docker Compose):
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "http://keycloak:8080/realms/master";
options.RequireHttpsMetadata = false; // Only for develop
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "http://localhost:8080/realms/master",
ValidateAudience = true,
ValidAudience = "net-web-api",
};
);
Don't forget to also add the proper middlewares:
app.UseAuthentication();
app.UseAuthorization();
And finally add the [Authorize]
attribute on the WeatherForecastController
:
[Authorize]
[ApiController]
[Route("/api/[controller]")]
public class WeatherForecastController : ControllerBase
Step 2 - Testing authorization
In the .NET API configuration, there is a field called ValidAudience="net-web-api"
, which means that only if the access token contains the audience net-web-api
, it will be allowed to access the resources. To enable this, we need to create a mapper. Go to the net-api-client
configuration, then navigate to Client Scopes, select the net-web-api-dedicated
scope, and create the following mapper of type Audience:
Now, fetch an access token from the following endpoint so you can test the authorization:
curl --location 'http://localhost:8080/realms/master/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=net-web-api' \
--data-urlencode 'client_secret=HXduWLurrenfHLadKG71P1GbUKH2HG04' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'audience=net-web-api'
The client_secret
can be found in the Credentials tab of the net-web-api
client inside the management console.
Finally, use the token obtained previously to make a call to the weatherforecast
endpoint, and you should be able to get the result:
curl --location 'http://localhost:5080/api/weatherforecast' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVTDlhN25Fc3kzb0RVMGFhSVdSM3pWYkNwdEdsVnZOMjhMMFprdUJBVXVrIn0.eyJleHAiOjE3NDAyNTI2NzYsImlhdCI6MTc0MDI1MjYxNiwianRpIjoiMzIwNDg1ODAtZjlkYS00MjAzLTgyZjUtMjlhN2Y3MWVhODFlIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOlsibmV0LXdlYi1hcGkiLCJhY2NvdW50Il0sInN1YiI6ImUzNWUyZGNmLTJmOGQtNDkxMC1hNWNkLWFiNmJmZDE1MWNhMCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im5ldC13ZWItYXBpIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19LCJuZXQtd2ViLWFwaSI6eyJyb2xlcyI6WyJ1bWFfcHJvdGVjdGlvbiJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImNsaWVudEhvc3QiOiIxNzIuMTguMC4xIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtbmV0LXdlYi1hcGkiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE4LjAuMSIsImNsaWVudF9pZCI6Im5ldC13ZWItYXBpIn0.Dv0u_z25t7YRrrCBtjjM6ETbmm7HuM2oAly3RBCpNFKMvellMieimFwUHfMIiKt0Ju6JUqr8KpTfX1aekBMSkRwcBDGTgs-TMByn-mNSawbTay1WAvrwYnSPPqgk4TJolmzFooNt-zw4uHAfBmf_Lg5KAwtM6_q2vTbUJmOUbUK-KupdwfT9q9poQ_ckBcnGAz3o-xAIMcwfnmOFWzF6aINZ6ZPrD4FiFeRrzXP6JePdvfFds3O514nFt4exV1rkEDXQMDTr7fK03TYQxXOlNXwOyWJf102eMRGwBxy7SQyibB0O9bIPZWUzjuXMP2kQ6hC9T1p-PZvUTYNUSOQz1g'
Conclusions
In this article, I have demonstrated how to configure a Keycloak server and use it to secure a .NET API. I plan to write another article where I will extend this example with a simple React application. In the following article, I will show you how to configure the React client app to integrate with the existing secure API and implement basic functionalities such as login, logout, viewing user profile data, and refresh token handling.
Appendix - Authentication flows
Flow | Description | Use Case |
---|---|---|
Standard Flow | OAuth 2.0 Authorization Code Flow. User logs in via Keycloak, client exchanges code for tokens. | Web apps with secure server-side storage. |
Direct Access Grants | Client sends username/password to get tokens directly. | Trusted apps (e.g., CLI tools, legacy systems). |
Implicit Flow | Tokens are returned directly after login (no code exchange). | Single-page apps (SPAs) or mobile apps (less secure, now discouraged). |
Service Accounts Roles | Client authenticates itself (no user) using client credentials to get tokens. | Machine-to-machine (M2M) or backend services. |
Device Authorization Grant | User authorizes a device via a code on another device. | Devices with limited input (e.g., smart TVs, IoT). |
OIDC CIBA Grant | Authentication happens on a separate device/channel without user interaction on the client. | Scenarios where user interaction isn’t possible (e.g., banking apps, IoT). |
Top comments (0)