Skip to main content
SEMastery
ASP.NETbeginner

Integrate Keycloak with ASP.NET Core Using OAuth 2.0

A beginner-friendly guide to securing an ASP.NET Core API and web app with Keycloak using OAuth 2.0 and OpenID Connect, with diagrams, tables, and copy-paste code.

11 min readUpdated May 1, 2026

A security guard at the office gate

Imagine a big office building in your city. At the front gate there is a security guard. When you arrive, you do not argue with every department about who you are. You go to the guard once, show your ID, and the guard gives you a visitor pass. After that, you simply show the pass at each floor. The people on each floor trust the pass because they trust the guard who issued it.

Keycloak is that security guard for your apps. It checks who the user is, then hands out a token (the visitor pass). Your ASP.NET Core API does not check passwords. It just looks at the token and trusts it, because it trusts Keycloak.

This is a very nice way to build software. You do not have to store passwords, build a login page, or handle "forgot password" emails. Keycloak does all of that. Your job is to trust its tokens. Let us learn how.

What is Keycloak?

Keycloak is a free, open-source identity and access management server. It is run by the CNCF community and used by companies all over the world. It gives you:

  • A ready-made login and sign-up page.
  • User accounts, roles, and groups.
  • Social login (Google, GitHub, and more).
  • Standard protocols: OAuth 2.0 and OpenID Connect (OIDC).

Because Keycloak speaks these standard protocols, ASP.NET Core can talk to it using the built-in authentication packages. You do not need a special "Keycloak SDK" for the basics.

Two words you will hear a lot

Before code, let us make two words simple.

TermPlain meaningReal-life version
AuthenticationProving who you areShowing your ID at the gate
AuthorizationDeciding what you can doThe pass only opens certain floors
OAuth 2.0A standard for giving access (authorization)The rules for issuing passes
OpenID ConnectA layer on OAuth 2.0 that adds login (authentication)The rule that the pass also proves your name
RealmA separate "world" of users in KeycloakOne building with its own guard and staff
ClientYour app, as registered inside KeycloakA tenant company in the building

Keep this table in mind. When you see "realm" later, just think "one building."

The big picture

Here is how the pieces fit together. The user logs in at Keycloak, gets a token, and then sends that token to your API.

Figure 1: The user logs in at Keycloak once, receives a token, and then sends that token to the ASP.NET Core API on each request.

The token is a JWT — a JSON Web Token. It is a small, signed string that says "this is user Ravi, and these are his roles." Because Keycloak signs it, your API can check the signature and know it was not faked.

Running Keycloak locally

The fastest way to try Keycloak is Docker. This one command starts a Keycloak server on port 8080 with an admin user.

docker run -p 8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:26.0 start-dev

Now open http://localhost:8080 and log in with admin / admin. Inside the admin console you will:

  1. Create a realm (for example, myrealm). This is your building.
  2. Create a client (for example, myapp). This is your app's identity.
  3. Create a user with a password, so you have someone to log in as.

For a web app that logs people in, set the client's access type to confidential and add a client secret. For a pure API that only checks tokens, you mostly care about the realm name and the audience.

Setting up the Keycloak client

When you create the client, a few settings matter. Here is a quick guide.

SettingWhat to putWhy it matters
Client IDmyappYour app's name inside the realm
Client authenticationOn (confidential)Requires a secret, safer for web apps
Valid redirect URIshttps://localhost:5001/signin-oidcWhere Keycloak sends the user back
Web originshttps://localhost:5001Allows the browser to talk to Keycloak
Valid post logout URIshttps://localhost:5001/signout-callback-oidcWhere to land after logout

The redirect URI is important. After login, Keycloak must send the user back to your app. If this URL does not match exactly, Keycloak refuses, and you see an error. Think of it like the guard only releasing your pass to the correct office address.

The login flow (Authorization Code with PKCE)

For a web app where a real person logs in, ASP.NET Core uses the Authorization Code flow with PKCE. PKCE (say it "pixie") is a safety check that stops attackers from stealing the login code in the middle.

Authorization Code flow with PKCE

Redirect
Login
Code back
Swap for tokens
Signed in

Steps

1

Redirect

App sends user to Keycloak

2

Login

User enters username and password

3

Code back

Keycloak returns a short-lived code

4

Swap for tokens

App exchanges code + secret for tokens

5

Signed in

App stores tokens in a cookie

The user is redirected to Keycloak, logs in, and comes back with a code that the app swaps for tokens.

Let us see the same flow as a sequence, so the order is crystal clear.

Figure 2: The full Authorization Code flow. The user logs in at Keycloak, and the app exchanges the code for tokens behind the scenes.

The good news: you do not write this flow by hand. The ASP.NET Core OpenID Connect handler does it for you. You only configure it.

Wiring up a web app with OpenID Connect

This is for an app that logs users in (like an MVC or Razor Pages app). Add these packages first.

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Now configure authentication in Program.cs. The app keeps the user signed in with a cookie, and uses Keycloak only at login time.

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    // The realm URL is the "Authority" — Keycloak's address for this building.
    options.Authority = "http://localhost:8080/realms/myrealm";
    options.ClientId = "myapp";
    options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
 
    options.ResponseType = "code";   // Authorization Code flow
    options.UsePkce = true;          // PKCE is on by default, shown here for clarity
    options.SaveTokens = true;       // Keep tokens so we can call APIs later
 
    options.Scope.Add("openid");
    options.Scope.Add("profile");
 
    // In dev only: Keycloak on http needs this. Never do this in production.
    options.RequireHttpsMetadata = false;
 
    options.GetClaimsFromUserInfoEndpoint = true;
});
 
builder.Services.AddAuthorization();
 
var app = builder.Build();
 
app.UseAuthentication();
app.UseAuthorization();
 
app.MapGet("/profile", (ClaimsPrincipal user) =>
        $"Hello, {user.Identity?.Name}!")
   .RequireAuthorization();
 
app.Run();

A few things to notice:

  • The Authority points to the realm, not the bare server. The handler reads Authority + /.well-known/openid-configuration to learn everything else.
  • UsePkce = true is the default, so you are safe even if you forget it.
  • RequireHttpsMetadata = false is only for local development with plain http. Turn it on in production.

When a user hits /profile, ASP.NET Core sees they are not logged in, sends them to Keycloak, and brings them back signed in. Lovely.

Securing an API that only checks tokens

Many real systems split things up. A front-end (maybe Angular or React) does the login. Then it calls a back-end API with the access token in the Authorization header. The API's only job is to validate the token — it never shows a login page.

For this, you use JWT Bearer authentication instead of OpenID Connect.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Here is the API setup. Notice how short it is.

var builder = WebApplication.CreateBuilder(args);
 
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "http://localhost:8080/realms/myrealm";
        options.RequireHttpsMetadata = false; // dev only
 
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "http://localhost:8080/realms/myrealm",
 
            ValidateAudience = true,
            ValidAudience = "account", // check the 'aud' claim of your token
 
            ValidateLifetime = true    // reject expired tokens
        };
    });
 
builder.Services.AddAuthorization();
 
var app = builder.Build();
 
app.UseAuthentication();
app.UseAuthorization();
 
app.MapGet("/orders", () => "Your secret orders 📦")
   .RequireAuthorization();
 
app.Run();

The most important idea: your API does not call Keycloak on every request. It downloads the public signing keys (the JWKS) once from the discovery endpoint, then checks each token's signature locally. That is why JWT validation is fast.

Figure 3: The API fetches Keycloak's signing keys once, then validates every token locally without calling Keycloak again.

Using roles from Keycloak

Authentication tells you who the user is. Authorization decides what they may do. Keycloak puts the user's roles inside the token. You can use them with policies.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminsOnly", policy =>
        policy.RequireRole("admin"));
});
 
app.MapGet("/admin", () => "Welcome, boss 👔")
   .RequireAuthorization("AdminsOnly");

One small catch: Keycloak nests realm roles under a realm_access.roles claim, which ASP.NET Core does not read as roles by default. You usually add a tiny piece of code that copies those values into proper role claims. Many teams write a small ClaimsTransformation for this, or map the claim type so RequireRole("admin") just works.

From request to allowed action

Request
Validate token
Check role
Run code

Steps

1

Request

Token in Authorization header

2

Validate token

Signature, issuer, audience, expiry

3

Check role

Does the user have 'admin'?

4

Run code

Return the protected data

Each protected request goes through authentication, then authorization, before the code runs.

Token types you should know

Keycloak gives out a few different tokens. Knowing them avoids confusion.

TokenWhat it is forLifetime
ID tokenProves who the user is (for the app)Short
Access tokenSent to APIs to gain accessShort (minutes)
Refresh tokenGets a new access token without re-loginLonger

The access token is the one your API checks. The refresh token lets the front-end stay logged in quietly. The ID token is mostly used by the web app to read the user's name and email.

Common mistakes and how to avoid them

A few errors trip up almost every beginner. Here is a short rescue list.

  • Wrong audience. If you see 401 even with a valid login, your ValidAudience probably does not match the aud claim. Decode the token at jwt.io and look at the real aud value, then set ValidAudience to that.
  • Redirect URI mismatch. Keycloak is strict. The redirect URI in the client must match your app's URL exactly, including https and the port.
  • Issuer mismatch. If your app reaches Keycloak at localhost but the token issuer says keycloak (a Docker hostname), validation fails. Keep the hostnames consistent.
  • Using http in production. RequireHttpsMetadata = false is fine on your laptop, but it must be true once you go live.

Quick recap

  • Keycloak is a free identity server. It is the security guard that checks users and hands out tokens, so your app never stores passwords.
  • A realm is one "building" of users; a client is your app's identity inside it.
  • For web apps that log people in, use OpenID Connect with the Authorization Code flow + PKCE. ASP.NET Core does the hard work; you just configure the Authority, ClientId, and ClientSecret.
  • For APIs that only verify tokens, use JWT Bearer. Set the Authority, ValidIssuer, and ValidAudience.
  • Your API validates tokens locally using Keycloak's signing keys — it does not call Keycloak on every request, so it stays fast.
  • Use roles from the token with authorization policies to control what each user can do.
  • Watch out for audience, issuer, and redirect URI mismatches — they cause most beginner errors.

References and further reading

Related Posts