OAuth 2.0 and OpenID Connect — The Concepts Every .NET Developer Needs
OAuth 2.0 and OIDC demystified: authorization code flow, tokens, scopes, the difference between authentication and authorization, and how ASP.NET Core integrates with external identity providers.
OAuth 2.0 is NOT Authentication
This is the most common confusion:
OAuth 2.0: authorization framework
"Can this application access these resources on behalf of this user?"
Result: access token (opaque or JWT)
Does NOT tell you WHO the user is
OpenID Connect (OIDC): authentication layer on top of OAuth 2.0
"Who is this user?"
Result: identity token (JWT with user info) + access token
Tells you the user's identity
Rule: if you need to know WHO is logged in → use OIDC, not bare OAuth 2.0The Authorization Code Flow (OIDC)
1. User clicks "Login with Microsoft/Google/Azure AD"
2. Browser redirects to identity provider:
GET https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize
?client_id=your-app-id
&response_type=code
&redirect_uri=https://yourapp.com/auth/callback
&scope=openid profile email
&state=random-csrf-token
&code_challenge=PKCE-challenge ← PKCE prevents code interception
3. User authenticates at the identity provider
4. Identity provider redirects back:
GET https://yourapp.com/auth/callback
?code=authorization-code
&state=random-csrf-token
5. Your server exchanges code for tokens:
POST /oauth2/v2.0/token
code=authorization-code
client_secret=your-secret
code_verifier=PKCE-verifier
6. Identity provider returns:
{ "id_token": "...", "access_token": "...", "refresh_token": "..." }
7. Your app validates the id_token, extracts the user identityASP.NET Core OIDC Integration
// Program.cs — add OIDC authentication
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code"; // authorization code flow
options.UsePkce = true; // PKCE required for public clients
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access"); // enables refresh tokens
options.SaveTokens = true; // save tokens in auth cookie
options.GetClaimsFromUserInfoEndpoint = true;
// Map external claims to internal claims
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapJsonKey("department", "extension_Department");
});Mixing JWT (for API) and OIDC (for Users)
Many systems need both: OIDC for browser users, JWT Bearer for API clients.
// Program.cs — multiple authentication schemes
builder.Services
.AddAuthentication()
.AddJwtBearer("Bearer", options =>
{
// Validates JWT tokens from your own token service
options.TokenValidationParameters = /* ... */;
})
.AddOpenIdConnect("AzureAD", options =>
{
// External login via Azure AD for admin portal
options.Authority = "https://login.microsoftonline.com/tenant";
// ...
});
// Default scheme selection per endpoint
app.MapGet("/api/patients", GetPatients)
.RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = "Bearer" });
app.MapGet("/admin/dashboard", GetDashboard)
.RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = "AzureAD" });Scopes and Resource Authorization
Scopes limit what an access token can do:
openid → include user identity claims in id_token
profile → include name, picture
email → include email address
patients.read → custom scope: read patient records
prescriptions.write → custom scope: write prescriptions
The resource server (your API) validates:
1. Token signature
2. Audience matches this API
3. Required scope is present
Example — validate scope in middleware:// Require a specific scope on an endpoint
app.MapGet("/patients", GetPatients)
.RequireAuthorization(policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("scope", "patients.read"));Token Types Comparison
ID Token (OIDC only):
Purpose: prove who the user is to YOUR application
Audience: your client application
Contains: sub, email, name, picture, custom claims
Use: read on login to establish session — do not send to APIs
Access Token:
Purpose: authorize access to a resource server (your API)
Audience: your API
Contains: sub, scope, role claims
Use: send in Authorization: Bearer header on every API call
Refresh Token:
Purpose: get new access tokens without re-authenticating
Never send to an API — only to the token endpointPKCE — Why It Matters
Without PKCE:
Attacker intercepts the authorization code (in the redirect URL)
Exchanges code for tokens → full access
With PKCE (Proof Key for Code Exchange):
Client generates: code_verifier (random) + code_challenge (SHA256 hash of verifier)
Sends code_challenge with authorization request
Sends code_verifier when exchanging code for tokens
Server verifies: SHA256(code_verifier) == code_challenge
Attacker has code but not code_verifier → cannot exchange it
PKCE is now required for all OAuth 2.0 clients (RFC 9700, 2025)
ASP.NET Core sets options.UsePkce = true by defaultRed Flag / Green Answer
Red Flag: "We validate the JWT access token coming from Azure AD by decoding it and checking the claims manually."
JWTs from Azure AD must be validated against Azure's JWKS endpoint (public keys). Manual decoding skips signature validation. A forged token with correct claims but invalid signature would pass your check.
Green Answer:
Use
AddJwtBearerwithoptions.Authority = "https://login.microsoftonline.com/tenant". The middleware fetches the JWKS, validates the signature automatically, and caches the keys with automatic rotation.
PRO TIP — Use Discovery Documents
Identity providers publish a
.well-known/openid-configurationendpoint. Settingoptions.Authorityin AddJwtBearer or AddOpenIdConnect causes ASP.NET Core to fetch this discovery document automatically. It contains the JWKS URI, supported scopes, token endpoint — everything needed for validation. You never hard-code these URLs.
Key Takeaway
OAuth 2.0 is authorization (what can this app do?). OIDC is authentication (who is this user?). Use the authorization code flow with PKCE for all browser-based applications. ASP.NET Core's
AddOpenIdConnecthandles the flow, PKCE, token validation, and claim mapping automatically. Mix JWT Bearer for API clients and OIDC for browser users with multiple schemes.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.