# Integrate Keycloak with ASP.NET Core Using OAuth 2.0

> Learn how to integrate Keycloak with your .NET 10 API using Docker, Swagger UI with OAuth 2.0 Authorization Code flow, and JWT validation.

Published: 2026-02-07. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/integrate-keycloak-with-aspnetcore-using-oauth-2

You can hand authentication to Keycloak instead of building it: run it as a container, create a realm with a public client, and point your .NET API at its metadata endpoint.
Swagger UI drives the OAuth 2.0 Authorization Code flow with PKCE, and the API validates the resulting JWT locally against Keycloak's cached signing keys.

**Authentication** is one of those things that's easy to get wrong and **expensive to fix later**.
Rolling your own auth system means dealing with password hashing, token management, session handling,
and a never-ending stream of security patches.

I was never a fan of this...

What if you could outsource all of that to a battle-tested **identity provider**?

[Keycloak](https://www.keycloak.org/) is an open-source identity and access management solution.
It handles user authentication, authorization, and identity brokering (social logins, enterprise SSO) out of the box.
You get a polished admin console, built-in support for **OAuth 2.0** and **OpenID Connect**, and it runs anywhere Docker does.

We'll spin up Keycloak as a container, create a realm with a public client,
and wire up Swagger UI to authenticate using the [OAuth 2.0](https://oauth.net/2/) **Authorization Code flow**.
Then we'll add [JWT](https://www.rfc-editor.org/rfc/rfc7519.html) validation to our .NET backend and
trace the entire authentication flow using the [**Aspire Dashboard**](https://milanjovanovic.tech/blog/standalone-aspire-dashboard-setup-for-distributed-dotnet-applications).

## Running Keycloak as a Container

The fastest way to spin up **Keycloak** is with Docker>).
We'll run it in development mode, which disables HTTPS and uses an embedded H2 database.
This is perfect for local development but **not suitable for production** (more on that later).

Here's a minimal `docker-compose.yml`:

```yaml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.5.2
    container_name: keycloak
    environment:
      - KC_BOOTSTRAP_ADMIN_USERNAME=admin
      - KC_BOOTSTRAP_ADMIN_PASSWORD=admin
    ports:
      - '8080:8080'
    command: start-dev
```

Start it with:

```bash
docker compose up -d
```

Once Keycloak is running, navigate to `http://localhost:8080` and log in with `admin` / `admin`.

![The Keycloak admin login screen with username and password fields.](https://milanjovanovic.tech/blogs/mnw_180/keycloak_admin_login.png)

You should see the Keycloak admin console.

![The Keycloak admin console showing the master realm dashboard.](https://milanjovanovic.tech/blogs/mnw_180/keycloak_admin_console.png)

## Setting Up a Realm and Client

Keycloak organizes everything into **realms**.
A realm is a space where you manage users, roles, and applications.
The `master` realm is reserved for Keycloak administration, so we'll create a new one for our application.

### Creating a Realm

1. Click the **Manage Realms** button in the top-left corner
2. Click **Create realm**
3. Enter a name (e.g., `keycloak-demo`) and click **Create**

![The Keycloak create realm dialog with ](https://milanjovanovic.tech/blogs/mnw_180/keycloak_create_realm_dialog.png)

### Creating a Public Client

Now we need to register our application.
In OAuth 2.0 terms, this is a **client**.
Since Swagger UI runs in the browser, we'll create a **public client** (no client secret).

1. Go to **Clients** → **Create client**
2. Set **Client ID** to `demo-api`
3. Leave **Client type** as `OpenID Connect`
4. Click **Next**

![The first step of creating a client in Keycloak, showing the Client ID field.](https://milanjovanovic.tech/blogs/mnw_180/keycloak_create_client_step1.png)

5. Enable **Client authentication**: Off (public client)
6. Check **Standard flow** (Authorization Code)
7. Choose **PKCE Method**: S256 (SHA-256)
8. Click **Next**

![The second step of creating a client showing authentication settings.](https://milanjovanovic.tech/blogs/mnw_180/keycloak_create_client_step2.png)

9. Configure the redirect URIs:
   - **Valid redirect URIs**: `https://localhost:5001/*` (your API's Swagger URL)
   - **Web origins**: `https://localhost:5001`
10. Click **Save**

![The third step showing redirect URI configuration.](https://milanjovanovic.tech/blogs/mnw_180/keycloak_create_client_step3.png)

### Creating a Test User

We need a user to authenticate with.

1. Go to **Users** → **Add user**
2. Fill in the details (username, email, etc.)
3. Leave **Email Verified** checked to avoid email confirmation
4. Click **Create**
5. Go to the **Credentials** tab
6. Click **Set password** and create a password (disable "Temporary")

![The Keycloak user creation form.](https://milanjovanovic.tech/blogs/mnw_180/keycloak_create_user.png)

You're now ready to authenticate users against Keycloak!

## The Authorization Code Flow

Before we dive into code, let's understand what happens when a user authenticates.
The [**Authorization Code flow**](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) is the recommended OAuth 2.0 flow for browser-based applications.

There's an important security enhancement called **PKCE** ([Proof Key for Code Exchange](https://www.rfc-editor.org/rfc/rfc7636))
that prevents authorization code interception attacks.
It works by having the client generate a random secret (the code verifier) and deriving a hash (the code challenge) sent in the initial authorization request.
When exchanging the authorization code for tokens, the client must present the original code verifier.

Here's the sequence:

![A sequence diagram showing the OAuth 2.0 Authorization Code flow between Browser, API, and Keycloak.](https://milanjovanovic.tech/blogs/mnw_180/authorization_code_flow.png)

1. **User clicks "Authorize"** in Swagger UI
2. **Browser redirects** to Keycloak's authorization endpoint
3. **User logs in** at Keycloak
4. **Keycloak redirects back** with an authorization code
5. **Swagger UI exchanges** the code for tokens (access token, refresh token, ID token)
6. **Swagger UI attaches** the access token to API requests
7. **API validates** the token signature and claims

The beauty of this flow is that credentials never touch your application.
The user authenticates directly with Keycloak, and your API only sees signed tokens.

## Configuring Swagger UI with OAuth 2.0

Now let's set up our .NET API to use Swagger UI as our OAuth 2.0 test client.

First, install the required packages:

```bash
dotnet add package Swashbuckle.AspNetCore
```

Configure Swagger in your `Program.cs`:

```csharp
var keycloakAuthority = builder.Configuration["Keycloak:Authority"]!;
var keycloakClientId = builder.Configuration["Keycloak:ClientId"]!;

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Demo API",
        Version = "v1"
    });

    // Define the OAuth 2.0 security scheme
    options.AddSecurityDefinition(nameof(SecuritySchemeType.OAuth2), new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri($"{keycloakAuthority}/protocol/openid-connect/auth"),
                TokenUrl = new Uri($"{keycloakAuthority}/protocol/openid-connect/token"),
                Scopes = new Dictionary<string, string>
                {
                    { "openid", "OpenID Connect scope" },
                    { "profile", "User profile" }
                }
            }
        }
    });

    // Apply security to all operations
    options.AddSecurityRequirement(doc => new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecuritySchemeReference(nameof(SecuritySchemeType.OAuth2), doc),
            []
        }
    });
});
```

And configure the Swagger UI middleware:

```csharp
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.OAuthClientId(keycloakClientId); // Default Client ID
        options.OAuthUsePkce(); // Proof Key for Code Exchange (security enhancement)
    });
}
```

Your `appsettings.Development.json`:

```json
{
  "Keycloak": {
    "Authority": "http://localhost:8080/realms/keycloak-demo",
    "ClientId": "demo-api",
    "Audience": "account",
    "Issuer": "http://localhost:8080/realms/keycloak-demo",
    // Here we use the Docker service name for Keycloak
    "MetadataAddress": "http://keycloak:8080/realms/keycloak-demo/.well-known/openid-configuration"
  }
}
```

Now when you open Swagger UI, you'll see an **Authorize** button.
Clicking it opens the OAuth flow, redirecting you to Keycloak to log in.

![Swagger UI showing the Authorize form for OAuth 2.0.](https://milanjovanovic.tech/blogs/mnw_180/swagger_authorize_form.png)

## Adding JWT Validation

At this point, Swagger UI can obtain tokens, but our API isn't validating them yet.
Let's add [**JWT Bearer authentication**](https://milanjovanovic.tech/blog/jwt-authentication-aspnetcore).

Install the authentication package:

```bash
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```

Configure authentication in `Program.cs`:

```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.MetadataAddress = builder.Configuration["Keycloak:MetadataAddress"]!;
        options.Audience = builder.Configuration["Keycloak:Audience"];

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = builder.Configuration["Keycloak:Issuer"]
        };

        // Required for HTTP in development (Keycloak uses HTTP by default in dev mode)
        options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
    });

builder.Services.AddAuthorization();
```

The default `TokenValidationParameters` will validate the token signature, expiration, issuer, and audience.

Add the middleware:

```csharp
app.UseAuthentication();
app.UseAuthorization();
```

Create a protected endpoint:

```csharp
app.MapGet("users/me", (ClaimsPrincipal user) =>
{
    return Results.Ok(new
    {
        UserId = user.FindFirstValue(ClaimTypes.NameIdentifier),
        Email = user.FindFirstValue(ClaimTypes.Email),
        Name = user.FindFirstValue("preferred_username"),
        Claims = user.Claims.Select(c => new { c.Type, c.Value })
    });
})
.RequireAuthorization();
```

## How JWT Validation Works

When a request hits your protected endpoint, here's what happens under the hood:

![A sequence diagram showing how JWT validation works in ASP.NET Core.](https://milanjovanovic.tech/blogs/mnw_180/jwt_validation_flow.png)

1. **Middleware extracts** the `Authorization: Bearer <token>` header
2. **JWT Handler fetches** Keycloak's public keys from the JWKS endpoint (cached)
3. **Signature validation** proves the token wasn't tampered with
4. **Claims are extracted** and the `ClaimsPrincipal` is populated
5. **Authorization middleware** checks if the user meets the endpoint requirements
6. **Endpoint executes** with access to `HttpContext.User`

The key insight here is that your API **never contacts Keycloak** to validate individual tokens.
It fetches the signing keys once and validates tokens locally.
This is what makes JWT-based authentication so fast.

## Observing the Flow with Aspire Dashboard

If you're using [**Aspire**](https://aspire.dev), you can observe the entire authentication flow in the distributed traces.

Here's what a successful authentication looks like:

![Aspire Dashboard showing a distributed trace of the authentication flow.](https://milanjovanovic.tech/blogs/mnw_180/aspire_auth_trace.png)

You can see:

1. The initial request to `users/me` (with the Bearer token)
2. The outbound call to Keycloak's `.well-known/openid-configuration` endpoint
3. The outbound call to Keycloak's JWKS endpoint (fetching signing keys)
4. The response back to the client

On subsequent requests, you won't see the JWKS call because the keys are cached.

This is why JWT validation adds virtually no latency after the initial key fetch.

## Production Considerations

What we've built is great for development.
For production, you'll want to address a few things:

**1. HTTPS Everywhere**

Keycloak should run behind HTTPS.
Set `KC_HOSTNAME` and configure TLS certificates.

**2. Persistent Storage**

Replace the embedded H2 database with PostgreSQL or MySQL:

```yaml
environment:
  - KC_DB=postgres
  - KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
  - KC_DB_USERNAME=keycloak
  - KC_DB_PASSWORD=secret
```

**3. Require HTTPS Metadata**

Remove `options.RequireHttpsMetadata = false` in production.

## Summary

In about 10 minutes, we've set up:

- A [**containerized**](https://milanjovanovic.tech/blog/containerize-your-dotnet-applications-without-a-dockerfile) Keycloak instance
- A realm with a public OAuth 2.0 client
- Swagger UI acting as an OAuth client with Authorization Code + PKCE
- JWT validation in ASP.NET Core
- Observability with [**OpenTelemetry**](https://milanjovanovic.tech/blog/introduction-to-distributed-tracing-with-opentelemetry-in-dotnet) into the authentication flow

What I really like about Keycloak is how easy it is to extend.
Want Google login? Configure it in Keycloak.
Need enterprise SSO? Add a SAML provider.
Your API code stays exactly the same because it just validates tokens.

If you want to see how I integrate Keycloak in a real-world system with role-based access control,
check out [**Pragmatic Clean Architecture**](https://milanjovanovic.tech/pragmatic-clean-architecture) and
[**Modular Monolith Architecture**](https://milanjovanovic.tech/modular-monolith-architecture).

See you next week.

---

## Frequently asked questions

### What is Keycloak?

Keycloak is an open-source identity and access management solution. It handles user authentication, authorization, and identity brokering (social logins, enterprise SSO) out of the box, ships a polished admin console, supports OAuth 2.0 and OpenID Connect, and runs anywhere Docker does.

### How do you run Keycloak locally?

Run the Keycloak container with Docker Compose in development mode (the start-dev command), which disables HTTPS and uses an embedded H2 database. Log in to the admin console on localhost:8080, then create a realm, a public client, and a test user. Development mode is not suitable for production.

### What is the OAuth 2.0 Authorization Code flow?

The recommended OAuth 2.0 flow for browser-based applications. The browser redirects to the identity provider, the user logs in there, the provider redirects back with an authorization code, and the client exchanges the code for tokens. Credentials never touch your application; the API only sees signed tokens.

### What is PKCE and why does it matter?

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. The client generates a random code verifier, sends a derived hash (the code challenge) in the initial authorization request, and must present the original verifier when exchanging the authorization code for tokens.

### Does an API call Keycloak to validate every JWT?

No. The JWT handler fetches Keycloak's public signing keys from the JWKS endpoint once, caches them, and validates token signatures locally. That is why JWT validation adds virtually no latency after the initial key fetch.

### What does Keycloak need for production use?

Run Keycloak behind HTTPS with KC_HOSTNAME and TLS certificates configured, replace the embedded H2 database with PostgreSQL or MySQL, and require HTTPS metadata in the ASP.NET Core JWT bearer options instead of the development-only HTTP setting.
