Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20265 min read
OAuth2OpenID ConnectASP.NET Core.NETAuthentication
Share:𝕏

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.0

The 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 identity

ASP.NET Core OIDC Integration

C#
// 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.

C#
// 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:
C#
// 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 endpoint

PKCE — 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 default

Red 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 AddJwtBearer with options.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-configuration endpoint. Setting options.Authority in 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 AddOpenIdConnect handles the flow, PKCE, token validation, and claim mapping automatically. Mix JWT Bearer for API clients and OIDC for browser users with multiple schemes.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.