OpenID in a .Net Web API with Keycloak

By Charles LAZIOSI
Published on

Introduction to OpenID Connect PKCE

OpenID Connect (OIDC) with PKCE (Proof Key for Code Exchange) is an extension to the OAuth 2.0 Authorization Code flow designed to improve security for public clients (such as single-page apps or mobile apps that cannot safely store client secrets) (Authorization Code Flow with Proof Key for Code Exchange (PKCE)) (OAuth 2.1 vs 2.0: What developers need to know). PKCE helps prevent authorization code interception attacks by requiring the client to prove possession of a secret (the code verifier) when exchanging the authorization code for tokens (What Is PKCE? | Postman Blog) (What Is PKCE? | Postman Blog). In a PKCE flow, the client app generates a random code verifier and a hashed version of it (the code challenge) before redirecting the user to the identity provider. The code challenge is sent in the login request. When the authorization code is later exchanged for tokens, the client must send the original code verifier. The authorization server (Keycloak in our case) will hash this verifier and compare it to the earlier code challenge – if they don’t match, the token request is rejected (What Is PKCE? | Postman Blog) (OAuth 2.1 vs 2.0: What developers need to know). This ensures that even if an attacker intercepts the one-time authorization code, they cannot exchange it for tokens without the code verifier secret (What Is PKCE? | Postman Blog).

PKCE is extremely important for securing OAuth/OIDC flows in modern applications. In fact, the upcoming OAuth 2.1 specification makes PKCE mandatory for all clients using the authorization code flow (even confidential clients) to mitigate code interception attacks (OAuth 2.1 vs 2.0: What developers need to know). By using PKCE, you can avoid older implicit grant flows and safely implement OIDC in public clients, obtaining access tokens and ID tokens in a secure manner. In summary, PKCE adds an extra layer of defense on top of OIDC’s authentication, making sure that the caller of the token endpoint is the same application that initiated the login, thus protecting your Web API from malicious tokens.

Configuring Keycloak for Authorization Code Flow + PKCE

Assuming you have an existing Keycloak Identity Provider (IdP) set up, you will need to configure a Client in Keycloak for your .NET 8 Web API (or for the frontend that will obtain tokens to call your API). The following steps guide you through Keycloak configuration, focusing on enabling PKCE and securing the client settings:

  1. Create a Client for your Application: In the Keycloak admin console, navigate to your realm and go to Clients, then click “Create”. Give the client a name (e.g., my-dotnet-app) and choose OpenID Connect as the protocol. For the client type, if your Web API is going to be called by a public client (like a JavaScript app), set Access Type to Public (no client secret). If the client is a confidential server-side app, you can use Confidential and set a client secret (the PKCE mechanism can be used in addition to a client secret for maximum security). In either case, ensure the Standard Flow (Authorization Code flow) is enabled and Implicit Flow is disabled. Also configure the valid redirect URIs and web origins for your application. For example, if you have a frontend at https://localhost:3000 and no dedicated login callback in the API, you might set the redirect URI to your frontend’s URL or a specific callback endpoint. (For pure API usage with tokens obtained out-of-band, the redirect URIs might not be used, but Keycloak requires at least one valid URI for the code flow.) Save the client to persist these initial settings.

  2. Enable PKCE (S256) in Client Settings: Keycloak supports PKCE out of the box and allows enforcing the use of PKCE. In your client’s settings, navigate to the Advanced tab. Locate the option “Proof Key for Code Exchange Code Challenge Method” and set it to S256 (the SHA-256 based challenge, which is much more secure than plain) (How to configure PKCE in KeyCloak server? - Stack Overflow). By selecting S256, Keycloak will require any authorization code requests for this client to use a PKCE code challenge with the SHA-256 method. This means clients must send a code_challenge and code_verifier to get tokens, adding security. The screenshot below shows where to configure this in Keycloak’s admin UI:

(Keycloak: How To Create A PKCE Authorization Flow Client?) Keycloak client Advanced settings, where PKCE (Code Challenge Method) is set to S256.

In addition, double-check other settings on the client: disable any flows you don’t need under Capability config (for example, turn Off “Direct Access Grants” unless you explicitly need Resource Owner Password credentials) (How to configure PKCE in KeyCloak server? - Stack Overflow), and ensure Service Accounts or others are off if not used. Disabling Direct Access Grants is a security best practice because the Resource Owner Password flow (direct username/password exchange) is deprecated and removing it reduces attack surface (How to configure PKCE in KeyCloak server? - Stack Overflow). Also, if your client is confidential, keep Client Authentication enabled (so that the token endpoint requires the client secret in addition to PKCE). If public, no client secret will be used. Finally, set Web Origins in Keycloak to the allowed origin of your front-end (or + to allow all if appropriate) to avoid CORS issues, and set any Admin URL if needed for logout or token push. Save the updated settings.

  1. Configure Roles and Permissions (RBAC/ABAC): In Keycloak, you can define roles globally (Realm Roles) or per-client (Client Roles) to implement Role-Based Access Control (RBAC). For example, you might create roles like admin, user, or manager in your realm. To do this, go to the Roles section in the Keycloak admin console and add new roles as needed. Next, assign these roles to your test users: navigate to Users, select a user, go to the Role Mappings tab, and add the appropriate roles (e.g., assign the admin role to an Admin user, and a user role to a normal user) (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium). These roles will be included in tokens that Keycloak issues. By default, Keycloak will include realm roles in the realm_access claim of the access token (and client roles in a resource_access claim for that client).

    For Attribute-Based Access Control (ABAC), Keycloak allows you to add user attributes and have them transmitted as claims in the token. For instance, you could add an attribute department=Finance to a user’s profile. To have this appear in the token, go to your client’s Mappers tab (or create a dedicated client scope with mappers) and create a User Attribute mapper: set the User Attribute to department and Token Claim Name to something like department (with inclusion in the Access Token and/or ID Token). This way, when the user authenticates, the JWT will contain a claim "department": "Finance" for that user. Roles are great for broad access control (e.g., “Admin” vs “User”), while attributes allow more fine-grained rules (e.g., access if department = Finance) (Best Practices for Implementing Permissions in Keycloak). Using both in combination allows implementing complex authorization checks in your API (Keycloak also has a policy engine for advanced use cases, but simply including roles and attributes in JWT claims is straightforward and often sufficient for app-level checks).

    Advanced Security Notes: While configuring Keycloak, consider setting short lifespans for tokens (in the Advanced settings, you can adjust Access Token Lifespan, e.g., maybe 5 or 10 minutes, depending on your needs) to limit how long a stolen token is valid (Best Practices for Implementing Permissions in Keycloak). Also ensure TLS/SSL is used – Keycloak’s Require SSL setting should be set to all requests in production (this is usually the default for Keycloak except possibly in development mode). We will discuss more best practices in a later section, but in summary: enforce PKCE (we’ve done this by requiring S256), avoid legacy grant types, use strong client secrets if applicable, and carefully define roles and attributes for least-privilege access.

Setting up the .NET Core 8 Web API for Keycloak Authentication

With Keycloak configured, the next step is to implement authentication and authorization in the .NET 8 Web API. We will use JWT Bearer authentication to validate the tokens issued by Keycloak. The high-level approach is:

  • Add the JWT Bearer authentication middleware, pointing it to the Keycloak realm for token validation.

  • Configure it to validate the issuer, audience, and signing key using Keycloak’s published metadata (so the API knows to trust only tokens from your Keycloak).

  • Enable authorization policies or attributes to enforce role and attribute checks (RBAC/ABAC) on API endpoints.

  • Read and use claims (like roles, username, department) from the JWT in your controllers as needed.

1. Install required NuGet packages

In your .NET project, ensure you have the authentication packages installed. In an ASP.NET Core Web API (especially .NET 8), the Microsoft.AspNetCore.Authentication.JwtBearer package is typically included by default. If not, add it via NuGet or the CLI:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Also ensure you have Microsoft.IdentityModel.Tokens (for token validation parameters) and any other needed packages. In .NET 8, these may already be part of the ASP.NET Core shared framework.

2. Configure JWT Authentication in Program.cs

Next, in your Web API’s startup code (for .NET 8 this is usually in Program.cs, since the generic host and minimal hosting model is used), configure the authentication services:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

// Add authentication services
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // URL of the Keycloak realm (base address for OIDC discovery)
        options.Authority = "https://<your-keycloak-domain>/realms/<your-realm-name>";
        // The Client ID configured in Keycloak for your app
        options.Audience = "my-dotnet-app";  // ensure this matches the client name or expected audience
        options.RequireHttpsMetadata = true; // ensure HTTPS in metadata (set false only for dev/test with HTTP)

        // Token validation parameters
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://<your-keycloak-domain>/realms/<your-realm-name>",
            ValidateAudience = true,
            ValidAudience = "my-dotnet-app",
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            // Name and Role claim type mapping (Keycloak uses "preferred_username" for username and often puts roles in 'realm_access')
            NameClaimType = "preferred_username",
            RoleClaimType = ClaimTypes.Role  // we'll map Keycloak roles to .NET roles later
        };

        // Optionally, for debugging/token logging, you can hook events here (omitted for brevity)
    });

// Add authorization services (policies for RBAC/ABAC)
builder.Services.AddAuthorization(options =>
{
    // Require Admin role (RBAC)
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("admin"));
    // Require Department = Finance (ABAC example)
    options.AddPolicy("DepartmentFinance", policy => 
        policy.RequireClaim("department", "Finance"));
});

// [Optional] If Keycloak roles are not directly in a claim that ASP.NET Core recognizes as roles, 
// you can use a claims transformation to map them. For example:
// builder.Services.AddScoped<IClaimsTransformation, KeycloakRolesClaimsTransformation>();
// (We'll assume roles are mapped for simplicity.)

var app = builder.Build();

// Use authentication & authorization middlewares
app.UseAuthentication();
app.UseAuthorization();

// Map your endpoints or controllers
// ...
app.MapControllers();

app.Run();

In the above configuration, we use AddJwtBearer with the Keycloak realm’s Authority. The Authority should be the realm URL (Keycloak’s OIDC issuer URL, typically https://<host>/realms/<realm>). By setting the Authority, the JwtBearer middleware will automatically download the realm’s OIDC discovery document (the .well-known/openid-configuration endpoint) which includes the public signing keys (JWKS), issuer name, token endpoints, etc. This allows .NET to automatically validate token signatures and issuer using Keycloak’s public keys (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium). We also set the expected Audience. In Keycloak, the audience of access tokens is usually the client ID or aliases of the client. By specifying options.Audience and enabling audience validation, we ensure the token presented is intended for our application (the token’s aud claim must match “my-dotnet-app” in this example) (Integrating Keycloak with .NET Core Web API 6 | by Omar Nebi | Medium).

The TokenValidationParameters further tighten the checks: we explicitly turn on issuer and lifetime validation. The ValidIssuer should match the iss claim in tokens exactly (Keycloak tokens will have iss = https://<host>/realms/<realm>). We set ValidateIssuerSigningKey to true which makes sure the token’s signature is validated against the keys from Keycloak’s JWKS. The signing key is fetched automatically from the Authority’s metadata (so you typically do not need to hardcode IssuerSigningKey unless you want to manually supply the public key; using Authority is easier and more maintainable).

We also configure the claim type mappings: Keycloak by default puts the username in the preferred_username claim, so we map .NameClaimType to that – this way User.Identity.Name in our API will return the username (or whatever is in preferred_username). For roles, Keycloak tokens include roles in a nested JSON structure by default (e.g., realm_access.roles array). We will handle that shortly. In the code above, we set RoleClaimType = ClaimTypes.Role (which is the string "role" URI under the hood). If we are able to map Keycloak roles into actual role claims, ASP.NET will treat those as roles for [Authorize(Roles="...")] checks.

Mapping Keycloak Roles: One simple approach is to configure Keycloak to map realm roles to a top-level claim. For example, in Keycloak’s client Mappers, you could add a “Realm Roles” mapper to map realm roles to the roles claim (as an array of strings). If you do that, you could set RoleClaimType = "roles" and ASP.NET will see each role in that claim as a role. Alternatively, you can use a claims transformation in .NET. For example, the code below (as an illustration) transforms the realm_access claim from Keycloak into individual role claims at runtime:

public class KeycloakRolesClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var identity = principal.Identity as ClaimsIdentity;
        var realmAccess = identity?.FindFirst("realm_access");
        if (realmAccess != null && identity != null)
        {
            // The realm_access claim contains JSON like: {"roles": ["user","admin"]}
            var data = System.Text.Json.JsonDocument.Parse(realmAccess.Value);
            if (data.RootElement.TryGetProperty("roles", out var rolesElem) && rolesElem.ValueKind == System.Text.Json.JsonValueKind.Array)
            {
                foreach (var role in rolesElem.EnumerateArray().Select(r => r.GetString()))
                {
                    if (!string.IsNullOrEmpty(role))
                        identity.AddClaim(new Claim(ClaimTypes.Role, role));
                }
            }
        }
        return Task.FromResult(principal);
    }
}

You would register this with builder.Services.AddScoped<IClaimsTransformation, KeycloakRolesClaimsTransformation>();. This is just to illustrate one way to get roles; in practice, adjusting the Keycloak token mapping might be easier. Either way, the end goal is that by the time a request reaches your controllers, User.IsInRole("admin") or [Authorize(Roles="admin")] works as expected by referencing the roles assigned in Keycloak.

Finally, we added two authorization policies as examples: AdminOnly requires the user have the “admin” role (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium), and DepartmentFinance requires the claim department equal to “Finance”. These demonstrate RBAC (role-based) and ABAC (attribute-based) checks respectively. You can add more policies as needed, or use the simpler [Authorize(Roles="...")] and [Authorize(Policy="...")] attributes directly on controllers or endpoints.

3. Protecting API Endpoints and Reading Claims

With the authentication configured, we can protect our API controllers or minimal API endpoints. In a controller-based approach, enable authentication globally by adding the [Authorize] attribute on the controller or specific actions. For example:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers;

[ApiController]
[Route("api/[controller]")]
public class SampleController : ControllerBase
{
    // Open endpoint (no auth needed)
    [HttpGet("public")]
    [AllowAnonymous]
    public IActionResult GetPublicData()
    {
        return Ok("This endpoint is public.");
    }

    // Any authenticated user can access
    [HttpGet("protected")]
    [Authorize]  // requires a valid token
    public IActionResult GetProtectedData()
    {
        string user = User.Identity?.Name ?? "unknown user";
        return Ok($"Hello {user}, you have access to protected data.");
    }

    // Only users with the 'admin' role can access (RBAC)
    [HttpGet("admin-data")]
    [Authorize(Roles = "admin")]
    public IActionResult GetAdminData()
    {
        return Ok("This is admin-only content.");
    }

    // Only users in Finance department can access (ABAC based on attribute)
    [HttpGet("finance-data")]
    [Authorize(Policy = "DepartmentFinance")]
    public IActionResult GetFinanceData()
    {
        var dept = User.FindFirst("department")?.Value;
        return Ok($"This is Finance Department data. Your department claim: {dept}");
    }
}

In the above example, we use [Authorize] in different ways. A plain [Authorize] on GetProtectedData means any valid, authenticated user (with a valid JWT) can access that endpoint. The GetAdminData uses [Authorize(Roles = "admin")] which will only let in users whose token contains the “admin” role (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium). The GetFinanceData uses a policy we configured earlier to only allow users who have a claim department: Finance.

Within the action methods, you can access the user’s claims via the User property (of type ClaimsPrincipal). For example, User.Identity.Name should give the username (because we mapped Name to preferred_username). User.FindFirst("department") gives the department attribute. User.IsInRole("admin") would be true if the role claim was set up. This shows how easily your code can make authorization decisions using the information from Keycloak’s token.

4. Validating Scopes (if using OAuth2 Scopes)

Keycloak primarily uses roles for authorization, but it also supports OAuth2 scopes and fine-grained permissions (when using Keycloak Authorization Services). If your design uses OAuth scopes (for example, maybe you define a scope like “myapi.read”), you should ensure the token contains the required scope for certain endpoints. Tokens from Keycloak will have a scope claim if the client uses consent or if scopes are requested. You can enforce scopes by adding policies similar to roles. For instance:

options.AddPolicy("CanReadData", policy => policy.RequireClaim("scope", "myapi.read"));

And then decorate an endpoint with [Authorize(Policy = "CanReadData")]. This will check that the scope claim in the JWT includes “myapi.read”. Make sure the client is actually requesting the scope and Keycloak is configured to issue it (this might involve client scopes in Keycloak). This approach is analogous to how roles are checked, but using a claim that lists scopes.

Best Security Practices for OIDC in Web API

Implementing OIDC with PKCE and Keycloak is a big step towards securing your application. Here are additional best practices to follow, focusing on security:

  • Always Use HTTPS: Ensure that all interactions with Keycloak and your Web API happen over HTTPS. HTTPS encrypts the authorization codes, tokens, and credentials in transit, preventing eavesdropping (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance). In .NET, you can enforce HTTPS by calling app.UseHttpsRedirection(); in your pipeline and, in production, using app.UseHsts(); to send HSTS headers (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance). Keycloak by default marks the realm as requiring SSL for external requests – keep that setting on. Never transmit tokens or credentials over plaintext HTTP.

  • Enforce PKCE Everywhere: We configured Keycloak to require PKCE (S256) for the client – do not disable this. On the client side (if you have a SPA or mobile app), ensure the OIDC library is actually using PKCE. For example, Keycloak’s JavaScript adapter has an option to enable PKCE (pkceMethod: "S256"). Most modern OAuth client libraries will handle PKCE automatically, but double-check it’s in use. This guarantees the benefits of PKCE are actually realized. We chose S256 because it’s recommended over the plain method (What Is PKCE? | Postman Blog). (Keycloak will allow plain only if explicitly configured, which we avoided.)

  • Validate the ID Token Nonce: If your Web API initiates an OIDC login (e.g., if it were a web app handling user login directly via OIDC), always validate the nonce. The nonce is a random value your app sends in the authorization request and expects back in the ID token. It is used to prevent replay attacks. The OpenID Connect spec mandates that if a nonce was in the request, the returned ID token MUST contain that same nonce value, and the client must verify it (OpenID Connect Core 1.0 incorporating errata set 2). Many libraries (including ASP.NET’s OpenID Connect handler or Keycloak’s adapters) handle nonce automatically. But if you ever deal with ID tokens manually, ensure nonce in the token matches what you sent (OpenID Connect (OIDC) on the Microsoft identity platform). This prevents an attacker from reusing an old token in your app.

  • Verify Issuer and Audience: We configured the JWT bearer options to validate issuer and audience. This is crucial – it ensures the token was issued by your Keycloak (and not some malicious or incorrect issuer) and that the token is meant for your API (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance). The issuer claim in the token should be the URL of your Keycloak realm, and the aud (audience) should equal the client or resource name you expect. The default behavior of AddJwtBearer with Authority already covers issuer validation by comparing to the discovery document’s issuer, but it’s good to explicitly set it as we did for clarity. Never disable issuer/audience validation in production, as that could allow tokens from other sources or contexts to be accepted.

  • Validate Token Signature and Lifetime: By using the JwtBearer middleware with Keycloak’s metadata, the token’s digital signature is automatically verified against Keycloak’s public keys (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium). This ensures the token hasn’t been tampered with and indeed comes from Keycloak. Additionally, always validate token expiration (exp claim) – which the middleware does via ValidateLifetime = true. This means requests with expired tokens will be rejected with 401 Unauthorized. Do not ignore expiration or allow excessively long-lived tokens. If you need long sessions, use refresh tokens to get new access tokens rather than making access tokens valid for hours. Keycloak issues refresh tokens by default in the code flow, and these should also be kept secure (never expose refresh tokens to the browser if you can avoid it; if using a public client like a SPA, consider using the Token Exchange or Refresh via the backend pattern to keep refresh tokens confidential).

  • Principle of Least Privilege in Tokens: When configuring what claims and roles go into the token, include only what’s necessary. For example, include roles and essential attributes but avoid putting extremely sensitive data in the token, since JWTs can be decoded by any recipient. Keycloak allows fine control via mappers and client scopes – use that to limit token content. Also, consider turning “Full Scope Allowed” off for the client if you want to restrict which realm roles get into the token (and then explicitly assign client scopes for needed roles). This reduces clutter in the token and potential misuse of privileges.

  • Use Security Headers in the API Responses: Even though our Web API typically returns JSON data and not HTML, security headers can still be important—especially if your API is ever accessed from a web context or has an interactive documentation (like Swagger UI). Add headers to prevent common attacks: for example, an ASP.NET Core middleware or configuration to add X-Content-Type-Options: nosniff (prevent MIME sniffing) and X-Frame-Options: DENY (prevent clickjacking by disallowing iframes) (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance). You might also add Content-Security-Policy headers if your API serves any HTML/JavaScript (or even to control embedded content in responses). Another header is Permissions-Policy: ... (formerly Feature-Policy) to disable any browser features not needed; in an API context, you might set it to none or other restrictive settings (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance). Since .NET 8, you can also easily configure a CORS policy – do so to only allow known origins to call your API, preventing random websites from invoking your endpoints with a user’s token. CORS doesn’t secure the token but prevents misuse in a browser context.

  • Use HSTS and Secure Cookies (if any): We mentioned HSTS for your site – sending Strict-Transport-Security header ensures browsers won’t downgrade to HTTP. If your web API uses cookies (perhaps not, if it’s stateless JWT auth; but maybe for something like session in Swagger UI), mark them as Secure and HttpOnly. Keycloak tokens won’t typically be in cookies (they are usually in the Authorization header as Bearer tokens), but if you ever set a cookie (like for a front-end app after OIDC login), ensure to set Secure and SameSite appropriately to avoid XSRF.

  • Regularly Update and Patch: Keep Keycloak updated to the latest version, as security fixes are included in updates. Similarly, keep your .NET runtime updated. OIDC libraries and frameworks are complex; staying up-to-date helps protect against known vulnerabilities.

  • Logging and Monitoring: Instrument your API to log authentication and authorization failures (but avoid logging sensitive token contents). Keycloak can also log events for logins, token refreshes, etc. Monitor these logs for suspicious activity (e.g., multiple failed token validations, which might indicate an attacker using a bad token). You can also use Keycloak’s admin events and endpoint to revoke tokens or set up detection for compromised tokens.

  • Test with Tools: Use tools like Postman or OAuth2-proxy to simulate obtaining tokens via PKCE and calling your API. For example, use Postman’s OAuth 2.0 authorization feature to perform a PKCE Authorization Code flow against Keycloak (by providing the Auth URL, Token URL, client ID, and toggling PKCE). Ensure that without the PKCE verifier, token requests fail. Also test that a user with insufficient roles cannot access an endpoint secured by an [Authorize] attribute – you should get HTTP 403 Forbidden in that case.

  • Additional Rate Limiting and Throttling: Consider enabling rate limiting on your API (ASP.NET 8 has built-in rate limiting middleware) to mitigate brute force or token guessing attacks. While JWTs if properly random and signed are not forgeable, an attacker might still try flooding your endpoints with different tokens. Rate limiting and perhaps IP blocking for abusive behavior can help (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance).

By following these practices, you ensure a defense-in-depth for your .NET Core Web API. We configured the Keycloak side to enforce modern OAuth standards (PKCE, no legacy grants) and set up roles/claims, and configured the .NET side to strictly validate the tokens and use those claims for authorization. The combination of PKCE-secured token issuance and robust token validation and authorization in the API will significantly harden your application’s security posture.

With the above setup, your .NET 8 Web API should only accept requests with valid JWT access tokens issued by your Keycloak server, and you have fine-grained control over what each user or service can do based on roles and attributes in those tokens. This achieves a clean separation of concerns: Keycloak manages authentication (user login, issuing tokens) and high-level authorization data, while your Web API focuses on business logic and enforcing the authorization rules using the information in the token.

Conclusion: You have now implemented OpenID Connect with PKCE using Keycloak in a .NET Core 8 Web API. You configured Keycloak to secure the OAuth flow with PKCE and set up roles/attributes for authorization, and you wrote the .NET code to authenticate and authorize incoming requests using JWT bearer tokens. Following the best practices outlined will help ensure that this integration remains secure against common threats and that your API only serves data to properly authenticated and authorized clients.

Sources:

  1. Postman Engineering – What Is PKCE? (What Is PKCE? | Postman Blog) (What Is PKCE? | Postman Blog)

  2. Auth0 Docs – Authorization Code Flow with PKCE (OAuth 2.1 vs 2.0: What developers need to know) (OAuth 2.1 vs 2.0: What developers need to know)

  3. Stack Overflow – Configuring PKCE in Keycloak (How to configure PKCE in KeyCloak server? - Stack Overflow) (How to configure PKCE in KeyCloak server? - Stack Overflow)

  4. Keycloak Authorization Best Practices (Permit.io) – Using Roles vs Attributes (Best Practices for Implementing Permissions in Keycloak)

  5. Medium (Amar Rai) – Keycloak .NET 8 Integration (RBAC Example) (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium) (Integrating Keycloak with .NET: A Simple Example of Authentication and Authorization (RBAC) | by Amar Rai | Medium)

  6. Medium (Omar Nebi) – .NET Core JWT with Keycloak (Integrating Keycloak with .NET Core Web API 6 | by Omar Nebi | Medium)

  7. i2bglobal Blog – Securing ASP.NET Core API Best Practices (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance) (Securing Your ASP.NET Core REST API: Best Practices for Safety & Performance)

  8. Microsoft Identity Platform Docs – OIDC and nonce validation (OpenID Connect (OIDC) on the Microsoft identity platform)

  9. OpenID Connect Core Spec – Nonce verification requirement (OpenID Connect Core 1.0 incorporating errata set 2)