ASP.NET Core OAuth2 and OpenID Connect: Complete Security Guide

Why OAuth2 + OpenID Connect for ASP.NET Core?

If you’re building APIs or web applications with ASP.NET Core in 2026, rolling your own authentication is almost always the wrong choice. ASP.NET Core OAuth2 and OpenID Connect (OIDC) are the industry-standard protocols that power secure authentication at Google, Microsoft, GitHub, and virtually every major platform and ASP.NET Core has built-in support for both.

Using these protocols means your application delegates trust to a proven identity provider (IdP) instead of storing raw passwords, implementing reset flows, and maintaining session state yourself. You get single sign-on (SSO), multi-factor authentication (MFA), and token-based security with few coding lines.

ASP.NET Core OAuth2 and OpenID Connect

Core Concepts: OAuth2 vs OpenID Connect

These two protocols are often confused because OIDC is built on top of OAuth2. Understanding the difference between both is very important before writing a single line of code.

ProtocolPurposeWhat it returnsUse case
OAuth 2.0Authorization, grant access to resourcesAccess TokenAPI access, scoped permissions
OpenID ConnectAuthentication, verify who the user isID Token (JWT) + Access TokenUser login, SSO, identity claims

In short: OAuth2 lets an app act on behalf of a user; OIDC proves who the user is. Most real-world applications need both OIDC to log the user in, and OAuth2 to call downstream APIs.

Choosing the Right Authorization Flow

OAuth2 defines several “flows” (also called grant types). Selecting the wrong one is a common security mistake.

FlowBest ForSecurity Level
Authorization Code + PKCEWeb apps, SPAs, mobile apps⭐⭐⭐⭐⭐ Recommended
Client CredentialsMachine-to-machine (M2M) services⭐⭐⭐⭐
Implicit (deprecated)Legacy SPAs avoid in new apps⭐⭐ Avoid
Resource Owner PasswordHighly trusted first-party apps only⭐⭐ Avoid if possible

Setting Up OAuth2 in ASP.NET Core

Step 1 — Install the required NuGet packages

# For JWT Bearer token validation
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

# For OpenID Connect (web app login)
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

# For cookie-based session persistence
dotnet add package Microsoft.AspNetCore.Authentication.Cookies

Step 2 — Configure appsettings.json

{
  "Authentication": {
    "Authority": "https://your-idp.example.com",
    "ClientId": "your-client-id",
    "ClientSecret": "<store-in-secrets-or-keyvault>",
    "Audience": "api://your-api-resource",
    "Scopes": ["openid", "profile", "email", "api.read"]
  }
}

Never commit ClientSecret to source control. Use dotnet user-secrets in development and Azure Key Vault or AWS Secrets Manager in production.

Adding OpenID Connect Authentication (Web App)

Program.cs

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

builder.Services
  .AddAuthentication(options =>
  {
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
  })
  .AddCookie(options =>
  {
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.ExpireTimeSpan = TimeSpan.FromHours(1);
    options.SlidingExpiration = true;
  })
  .AddOpenIdConnect(options =>
  {
    options.Authority   = config["Authentication:Authority"];
    options.ClientId    = config["Authentication:ClientId"];
    options.ClientSecret = config["Authentication:ClientSecret"];
    options.ResponseType = "code";          // Authorization Code flow
    options.UsePkce     = true;              // Always enable PKCE
    options.SaveTokens  = true;              // Store tokens in cookie
    options.GetClaimsFromUserInfoEndpoint = true;

    // Request the scopes your app needs
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("api.read");

    // Map ID token claims to ASP.NET Core claims
    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name,  "name");
  });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();  // Must come before UseAuthorization
app.UseAuthorization();

app.MapControllers();
app.Run();

Validating JWT Bearer Tokens (API Projects)

For pure API projects that don’t serve HTML (such as a Web API consumed by a SPA or mobile app), use JWT Bearer authentication instead of OIDC + cookies. The API simply validates the token issued by your identity provider.

Program.cs (API project)

builder.Services
  .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
    options.Authority = config["Authentication:Authority"];
    options.Audience  = config["Authentication:Audience"];

    options.TokenValidationParameters = new TokenValidationParameters
    {
      ValidateIssuer           = true,
      ValidateAudience         = true,
      ValidateLifetime         = true,
      ValidateIssuerSigningKey = true,
      ClockSkew                = TimeSpan.FromSeconds(30),
    };

    // Optional: log detailed auth failures in development
    options.Events = new JwtBearerEvents
    {
      OnAuthenticationFailed = ctx =>
      {
        var logger = ctx.HttpContext
            .RequestServices
            .GetRequiredService<ILogger<Program>>();
        logger.LogWarning("JWT auth failed: {Error}", ctx.Exception.Message);
        return Task.CompletedTask;
      }
    };
  });

Setting options.Authority automatically fetches the IdP’s discovery document (/.well-known/openid-configuration) and validates signing keys. You don’t need to hard-code public keys.

Authorization Policies and Claims

Once a user is authenticated, use policy-based authorization to control what they can do. Policies are more flexible than [Authorize(Roles = "Admin")] and let you combine claims, scopes, and custom requirements.

Program.cs — define policies

builder.Services.AddAuthorization(options =>
{
  // Require a specific OAuth2 scope
  options.AddPolicy("ReadData", policy =>
    policy.RequireClaim("scope", "api.read"));

  // Require role from ID token claims
  options.AddPolicy("AdminOnly", policy =>
    policy.RequireRole("Admin"));

  // Combine multiple requirements
  options.AddPolicy("PowerUser", policy =>
  {
    policy.RequireAuthenticatedUser();
    policy.RequireClaim("scope", "api.write");
    policy.RequireClaim("email_verified", "true");
  });
});
OrdersController.cs — apply policies

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
  [HttpGet]
  [Authorize(Policy = "ReadData")]
  public IActionResult GetOrders() { /* ... */ }

  [HttpPost]
  [Authorize(Policy = "AdvanceUser")]
  public IActionResult CreateOrder() { /* ... */ }

  [HttpDelete("{id}")]
  [Authorize(Policy = "AdminOnly")]
  public IActionResult DeleteOrder(int id) { /* ... */ }
}

Production Security Best Practices

Always use PKCE with the Authorization Code Flow

Store secrets in a vault, never in appsettings

Validate token audience and issuer

Always set ValidateIssuer = true and ValidateAudience = true. An attacker who obtains a token from one of your other services should not be able to use it on this API.

Set short token lifetimes + use refresh tokens

Access tokens should live 5–15 minutes. Pair them with refresh tokens (stored securely, ideally rotated on every use) for a seamless user experience without long-lived vulnerable tokens.

Enable HTTPS everywhere + HSTS

Call app.UseHsts() in production and app.UseHttpsRedirection(). OAuth2 tokens in transit over HTTP is a critical vulnerability.

Implement token revocation for sensitive logout

On sign-out, call your IdP’s revocation endpoint (/connect/revocation) to invalidate refresh tokens — not just clear the local cookie.

Audit and log auth events

Hook into JwtBearerEvents and OpenIdConnectEvents to emit structured logs on failures, token refresh, and policy denials. Feed them into your SIEM.

Security Checklist Before Going Live

  • Authorization Code + PKCE flow is used (not Implicit)
  • Client secret is stored in a vault / environment variable — not in source code
  • Token issuer and audience are validated on every request
  • Access token lifetime is 15 minutes or less
  • Refresh tokens are rotated and stored securely (HttpOnly cookie or encrypted store)
  • HTTPS is enforced app-wide; HSTS is configured
  • Cookies use HttpOnlySecure, and SameSite=Lax
  • CORS is configured to allow only your known origins
  • Auth failures are logged with correlation IDs
  • Token revocation endpoint is called on logout
  • Discovery document is cached (not fetched on every request)
  • Rate limiting / brute-force protection is enabled on your IdP

Leave a Reply

Your email address will not be published. Required fields are marked *