🔐 Authentication in .NET Web Applications
A deep dive into all authentication mechanisms – from classic forms to modern OAuth2/OIDC, JWT internals, symmetric vs asymmetric keys, and practical flows.
📑 Table of Contents
1. Overview
Authentication is the process of verifying who a user is. In .NET web applications (ASP.NET Core, classic ASP.NET, Blazor, etc.) we have multiple authentication strategies that range from simple username/password exchanges to sophisticated federated identity protocols. Choosing the right one depends on application architecture, security requirements, and client types (browser, mobile, machine‑to‑machine).
This guide explores each mainstream authentication type, explains the human‑readable and cryptographic internals of JSON Web Tokens (JWT), and details the role of symmetric (HMAC) and asymmetric (RSA/ECDSA) keys. We also dive into OAuth 2.0 and OpenID Connect, the backbone of modern delegated authorization and single sign‑on.
2. Forms Authentication (Cookie‑based)
Concept: The user submits credentials via an HTML form. The server validates them and, on success, creates a session and sets an encrypted authentication cookie. Subsequent requests carry the cookie, which the server deserializes to reconstruct the user identity.
How it works in .NET (ASP.NET Core)
Using the AddCookie authentication handler. The cookie is encrypted using the Data Protection API and often contains claims like user ID, name, and roles.
// Startup.cs / Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(2);
});Authentication Flow
/Account/Login.2. User submits username/password.
3. Server validates credentials (e.g., against ASP.NET Core Identity).
4. Server creates a
ClaimsPrincipal, builds a ClaimsIdentity, and calls HttpContext.SignInAsync().5. An encrypted cookie (e.g., .AspNetCore.Cookies) is sent back in the response.
6. Every subsequent request includes the cookie. The authentication middleware decrypts it, validates expiration, and sets
HttpContext.User.Security notes: Cookies should be marked HttpOnly and Secure (HTTPS only). Use sliding expiration and consider SameSite settings to mitigate CSRF.
3. Windows Authentication (Kerberos / NTLM)
Used in intranet scenarios where both client and server are part of the same Active Directory domain (or trusted domains). The browser automatically negotiates credentials without a login form.
Protocols: Kerberos (default in modern Windows) and NTLM (fallback). The server challenges the client with a WWW-Authenticate: Negotiate header.
Flow (simplified Kerberos)
WWW-Authenticate: Negotiate.2. Browser obtains a Kerberos service ticket from the domain controller for the target server's SPN.
3. Browser sends the ticket in an
Authorization: Negotiate <token> header.4. Server validates the ticket (using its machine account) and extracts the user's identity (domain\username).
5. ASP.NET Core's
AddNegotiate (or IIS Windows Authentication) creates a WindowsPrincipal..NET Implementation: In ASP.NET Core, use AddNegotiate() or enable Windows Authentication in IIS/Kestrel. Impersonation is optional.
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();Windows auth is not suitable for external clients or cross‑platform scenarios where Kerberos infrastructure isn't available.
4. Basic Authentication
A simple, standard (RFC 7617) mechanism where the client sends Authorization: Basic <base64(username:password)> with every request. The server decodes and validates the credentials.
Critical: Credentials are only base64‑encoded, not encrypted – TLS/HTTPS is mandatory.
Flow
WWW-Authenticate: Basic realm="MyApp".2. Browser shows a built‑in login dialog (or the client manually constructs the header).
3. Client sends request with
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==.4. Server base64‑decodes the header, splits on first colon, and validates against a user store.
5. If valid, the request proceeds; otherwise 401 again.
In .NET, you can implement a custom AuthenticationHandler that reads the Authorization header. There's no built‑in Basic handler in ASP.NET Core (for good reason – it's too simplistic for most apps).
5. Token‑Based Authentication with JWT
Instead of server‑side sessions, the server issues a self‑contained JSON Web Token (JWT) after successful login. The token contains claims (user info, expiry) and is cryptographically signed. The client stores the JWT (typically in memory or httpOnly cookie for web apps) and sends it as a Bearer token in the Authorization header. The server validates the signature on each request without needing to query a session store.
5.1 JWT Structure & Internals
A JWT consists of three Base64Url‑encoded parts separated by dots: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c- Header: Contains the token type (
typ: "JWT") and signing algorithm (alg: "HS256"orRS256). - Payload: The claims set, e.g.,
sub(subject),name,iat(issued at),exp(expiration), custom claims. Claims are not encrypted, only base64‑encoded – never put secrets here. - Signature: Computed over
header + "." + payloadusing the secret key (symmetric) or private key (asymmetric). Ensures integrity and authenticity.
Example decoded payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}5.2 Symmetric Key Signing (HMAC)
Uses a single shared secret key between issuer and verifier. Algorithms: HS256, HS384, HS512 (HMAC with SHA‑256/384/512).
The issuer and the API share the exact same key (e.g., a 256‑bit random string). The signature is created as
HMACSHA256(secret, header + "." + payload). Anyone with the secret can create or verify tokens – compromise means total token forgery.Symmetric keys are simpler and faster but are only suitable when a single service both issues and validates tokens, or when the key can be securely distributed to a small number of trusted services.
// .NET JWT validation with symmetric key
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-256-bit-secret"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = "myapp",
ValidateAudience = true,
ValidAudience = "myapp"
};
});5.3 Asymmetric Key Signing (RSA / ECDSA)
Uses a private key to sign the token, and a public key to verify. Algorithms: RS256, RS384, RS512 (RSA), ES256, ES384, ES512 (ECDSA).
The token issuer (authorisation server) signs JWTs with its private key. Consumers (APIs) validate using the corresponding public key, often obtained via a JWKS (JSON Web Key Set) endpoint. The private key never leaves the issuer – no shared secret.
This is the standard for multi‑service architectures, OAuth2/OpenID Connect, and third‑party integrations. .NET can load public keys from a JWKS URL automatically.
// Validation using a public RSA key (from JWKS or certificate)
var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem);
var key = new RsaSecurityKey(rsa);
// ... use in TokenValidationParameters5.4 JWT Authentication Flow (typical SPA / Mobile API)
POST /api/auth/login).2. Server verifies credentials, generates a JWT (signed with chosen key).
3. Server returns the JWT (optionally a refresh token as well) in the response body.
4. Client stores the JWT (e.g., in memory, secure cookie, or
localStorage – with security caveats).5. For each API call, client sets
Authorization: Bearer <token>.6. Server's JWT middleware extracts the token, validates signature, expiry, issuer, audience, and populates
HttpContext.User.Refresh tokens handle token expiry without re‑authenticating: a long‑lived opaque token stored securely, used to obtain a new JWT when the access token expires.
6. OAuth 2.0 & OpenID Connect
6.1 OAuth 2.0 – Delegated Authorization
OAuth 2.0 is a framework that allows a user to grant a third‑party application limited access to their resources without sharing credentials. It defines four roles:
- Resource Owner (the user)
- Client (the application requesting access)
- Authorization Server (issues tokens, e.g., IdentityServer, Azure AD)
- Resource Server (the API hosting protected data)
OAuth 2.0 specifies several grant types (flows):
| Grant Type | Use Case |
|---|---|
| Authorization Code | Server‑side web apps (most secure, uses code exchange) |
| Implicit (deprecated) | SPA (legacy, tokens directly in URL fragment) |
| Client Credentials | Machine‑to‑machine communication |
| Resource Owner Password (deprecated) | Trusted first‑party apps (username/password directly) |
| Device Code | Input‑constrained devices (TVs, IoT) |
OAuth does not provide identity information – that's where OpenID Connect steps in.
6.2 OpenID Connect (OIDC) – Identity Layer on Top of OAuth 2.0
OIDC adds an ID Token (a JWT) and a UserInfo endpoint to OAuth 2.0. It enables the client to verify the user's identity and obtain basic profile information.
Key OIDC artifacts:
- ID Token: JWT containing claims about the authentication event (sub, iss, aud, exp, etc.).
- Access Token: OAuth 2.0 token for accessing the resource server (can be JWT or opaque).
- UserInfo Endpoint: Returns additional claims (email, name, picture) using the access token.
- Discovery Document:
/.well-known/openid-configuration– provides all endpoint URLs and supported capabilities.
6.3 ID Token vs Access Token
| Property | ID Token | Access Token |
|---|---|---|
| Purpose | Proves user authentication | Grants access to resources |
| Audience | The client application | The resource server / API |
| Format | Always JWT | JWT (structured) or opaque reference |
| Contains | User identity claims (sub, email) | Scopes, permissions, maybe user id |
| Validation | Client validates signature & audience | Resource server validates signature, scopes |
6.4 OIDC Flows & .NET Integration
OIDC extends OAuth flows with the openid scope. The most common and secure flow is the Authorization Code Flow with PKCE (Proof Key for Code Exchange), mandatory for SPAs and recommended for all public clients.
Authorization Code Flow (PKCE) – Step by Step
code_verifier and a code_challenge (SHA‑256 hash).2. Client redirects user to the authorization server's authorize endpoint with
response_type=code, scope=openid profile, code_challenge.3. User authenticates and consents; authorization server redirects back with an authorization code.
4. Client sends the code +
code_verifier to the token endpoint (back‑channel).5. Server validates the code and PKCE, returns an ID token, access token, and (optionally) a refresh token.
6. Client validates the ID token (signature, issuer, audience, nonce) and extracts user claims.
7. Client uses the access token to call APIs.
In ASP.NET Core, the Microsoft.AspNetCore.Authentication.OpenIdConnect package handles this flow transparently. Configuration example:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://identity.example.com";
options.ClientId = "webapp";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.UsePkce = true;
options.SaveTokens = true;
});Key Distribution with JWKS
The authorization server publishes its public keys at a JWKS endpoint (e.g., /.well-known/jwks.json). The JWT bearer middleware can be configured to automatically fetch and rotate keys:
options.Authority = "https://identity.example.com"; // auto‑discovers JWKSThis relies on asymmetric signing – the server signs with its private key, APIs verify with the public key from the JWKS.
7. Symmetric vs Asymmetric Keys in Authentication – Deeper View
Symmetric (shared secret)
- Algorithms: HS256, HS384, HS512.
- How it works: Same key for signing and verification.
- Performance: Very fast (HMAC is lightweight).
- Security: A single secret must be protected across all consumers. A leak anywhere compromises the entire system.
- Use cases: Simple monoliths, internal services where one team controls both issuer and consumer.
- Key rotation: Difficult because all parties must update simultaneously.
Asymmetric (public/private key pair)
- Algorithms: RS256/384/512, ES256/384/512, PS256.
- How it works: Issuer holds private key; verifiers only need public key.
- Performance: Slower signing (RSA) but verification is fast; ECDSA is faster across the board.
- Security: Private key remains isolated. Compromise of a public key or a consumer does not allow token forgery.
- Use cases: Multi‑service, OAuth2/OIDC providers, cloud‑native environments.
- Key rotation: Easier – issuer can publish new public keys in JWKS while old ones are still trusted for a transition period.
8. Comparison of Authentication Types
| Type | State | Token Format | Best For | Key Management | Standard |
|---|---|---|---|---|---|
| Forms / Cookie | Server session (cookie) | Encrypted cookie | Traditional server‑rendered apps | Data Protection API | – |
| Windows | Per‑request ticket | Kerberos/NTLM token | Intranet, AD domain | Domain trust | SPNEGO |
| Basic | Stateless (sent each time) | Base64 user:pass | Simple APIs over HTTPS | None (just password) | RFC 7617 |
| JWT (Bearer) | Stateless | Signed JWT | REST APIs, SPAs, microservices | Symmetric or asymmetric | RFC 7519 |
| OAuth2 + OIDC | Hybrid (tokens + session) | JWT ID/access tokens | SSO, delegated auth, federation | Asymmetric (JWKS) | OAuth2 RFC 6749, OIDC |
9. .NET Implementation Considerations
ASP.NET Core provides a unified authentication model. You can combine multiple schemes (e.g., cookie + JWT) and set a default policy.
builder.Services.AddAuthentication()
.AddCookie("Cookies")
.AddJwtBearer("Bearer", options => { ... });
// Default:
options.DefaultAuthenticateScheme = "Cookies";
options.DefaultChallengeScheme = "Bearer";For OIDC, the middleware handles the entire code flow, token refresh, and even session management. Always use PKCE for public clients (SPAs, mobile).
Production checklist:
- Use HTTPS everywhere.
- Set short access token lifetime (5‑15 min) and use refresh tokens.
- Store secrets in Azure Key Vault, HashiCorp Vault, or user secrets (dev).
- Validate all standard claims:
iss,aud,exp,nbf. - For JWTs, avoid sensitive data in payload unless encrypted (JWE).
- Rotate signing keys regularly, and use key IDs (
kidheader) to support multiple keys.
10. Conclusion
Authentication in .NET web applications is a vast landscape, from the simplicity of Windows Integrated Authentication to the flexibility of JWT‑based stateless APIs and the federation power of OpenID Connect. Understanding the internals of JWTs, the difference between symmetric and asymmetric keys, and the various OAuth2/OIDC flows enables you to design secure, scalable identity solutions. ASP.NET Core’s extensible authentication middleware and first‑class support for JWT and OIDC make implementing these patterns straightforward and production‑ready.
For any modern .NET application that exposes an API or requires single sign‑on, adopting OIDC with asymmetric key signing and PKCE is the recommended path.
© 2025 – Expert .NET Authentication Guide. All information is based on current industry standards and ASP.NET Core 8+.