OAuth 2.0 and OpenID Connect are everywhere, yet confusion between the two remains widespread. OAuth 2.0 is an authorisation framework — it lets an application access resources on behalf of a user. OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 — it tells you who the user is. In practice, you almost always use them together.

The Authorization Code Flow with PKCE

For web applications, the Authorization Code flow with PKCE (Proof Key for Code Exchange) is the recommended approach. ASP.NET Core handles most of the heavy lifting:

Program.cs
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    options.Authority = "https://login.example.com";
    options.ClientId = builder.Configuration["Oidc:ClientId"]!;
    options.ClientSecret = builder.Configuration["Oidc:ClientSecret"]!;
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");

    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    };
});

Setting ResponseType to "code" triggers the authorization code flow. The middleware automatically handles PKCE, token exchange, and cookie creation.

Understanding the Token Types

OIDC gives you three tokens, each with a distinct purpose:

When SaveTokens is true, you can retrieve them later:

Program.cs
app.MapGet("/api/call-external", async (HttpContext context, HttpClient httpClient) =>
{
    var accessToken = await context.GetTokenAsync("access_token");

    if (accessToken is null)
        return Results.Unauthorized();

    httpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", accessToken);

    var response = await httpClient.GetAsync("https://api.example.com/data");
    var content = await response.Content.ReadAsStringAsync();

    return Results.Ok(content);
});

Integrating with External Providers

ASP.NET Core ships with built-in support for common providers. For Microsoft Entra ID (formerly Azure AD):

Program.cs
builder.Services.AddAuthentication()
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization();

For Google or GitHub, use the respective NuGet packages:

Program.cs
builder.Services.AddAuthentication()
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Google:ClientId"]!;
        options.ClientSecret = builder.Configuration["Google:ClientSecret"]!;
    });

Handling Claims Mapping

Identity providers return claims in varying formats. You'll often need to map them to something your application understands:

Example.cs
.AddOpenIdConnect(options =>
{
    // ...other config...

    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
    options.ClaimActions.MapJsonKey("picture", "picture");

    options.Events = new OpenIdConnectEvents
    {
        OnTokenValidated = context =>
        {
            var claims = context.Principal?.Claims
                .Select(c => $"{c.Type}: {c.Value}");

            // Log claims for debugging during development
            Console.WriteLine(string.Join("\n", claims ?? []));
            return Task.CompletedTask;
        }
    };
});

The OnTokenValidated event is invaluable during development. It lets you inspect exactly which claims arrive so you can map them correctly.

Securing APIs with OAuth Bearer Tokens

If you're building an API rather than a web app, you'll validate incoming bearer tokens instead:

Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com";
        options.Audience = "my-api";
    });

Setting Authority tells the middleware to fetch the provider's discovery document at /.well-known/openid-configuration, which contains the signing keys. No need to configure keys manually — the middleware handles key rotation automatically.

Common Pitfalls

Confusing authentication with authorisation. OIDC tells you who someone is. What they're allowed to do is a separate concern handled by your authorisation policies.

Not validating the audience claim. Without audience validation, a token issued for one API could be used against another. Always set and validate Audience.

Using the implicit flow. The implicit flow returns tokens directly in the URL fragment, exposing them in browser history and logs. Authorization code flow with PKCE is the modern standard for all client types.

Wrapping Up

The ASP.NET Core middleware handles the complexity of OAuth 2.0 and OIDC flows remarkably well. Your job is to configure it correctly: use the authorization code flow with PKCE, validate audiences, map claims sensibly, and store tokens securely. Get those fundamentals right and the framework takes care of the rest.