Authentication in .NET Web Applications

 

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

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

1. User navigates to a protected resource → redirected to /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)

1. Browser requests a resource → server returns 401 with 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

1. Client requests a resource → server returns 401 with 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" or RS256).
  • Payload: The claims set, e.g., sub (subject), nameiat (issued at), exp (expiration), custom claims. Claims are not encrypted, only base64‑encoded – never put secrets here.
  • Signature: Computed over header + "." + payload using 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: HS256HS384HS512 (HMAC with SHA‑256/384/512).

🔑 Symmetric Key Example (HMAC‑SHA256):
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: RS256RS384RS512 (RSA), ES256ES384ES512 (ECDSA).

🔑 Asymmetric Key Pair:
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 TokenValidationParameters

5.4 JWT Authentication Flow (typical SPA / Mobile API)

1. Client sends credentials to a login endpoint (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 TypeUse Case
Authorization CodeServer‑side web apps (most secure, uses code exchange)
Implicit (deprecated)SPA (legacy, tokens directly in URL fragment)
Client CredentialsMachine‑to‑machine communication
Resource Owner Password (deprecated)Trusted first‑party apps (username/password directly)
Device CodeInput‑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

PropertyID TokenAccess Token
PurposeProves user authenticationGrants access to resources
AudienceThe client applicationThe resource server / API
FormatAlways JWTJWT (structured) or opaque reference
ContainsUser identity claims (sub, email)Scopes, permissions, maybe user id
ValidationClient validates signature & audienceResource server validates signature, scopes
⚠️ Important: Never use the access token as proof of authentication. The ID token is the proper credential for user identity assertion in OIDC.

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

1. Client generates a code_verifier and a code_challenge (SHA‑256 hash).
2. Client redirects user to the authorization server's authorize endpoint with response_type=codescope=openid profilecode_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 tokenaccess 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 JWKS

This 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.
🔐 Best practice for OIDC/enterprise: Use RS256 or ES256. The authorization server signs with a private key; APIs validate with the JWKS. For internal microservice‑only JWT, a symmetric key may be simpler but requires careful secret management (vault, environment variables).

8. Comparison of Authentication Types

TypeStateToken FormatBest ForKey ManagementStandard
Forms / CookieServer session (cookie)Encrypted cookieTraditional server‑rendered appsData Protection API
WindowsPer‑request ticketKerberos/NTLM tokenIntranet, AD domainDomain trustSPNEGO
BasicStateless (sent each time)Base64 user:passSimple APIs over HTTPSNone (just password)RFC 7617
JWT (Bearer)StatelessSigned JWTREST APIs, SPAs, microservicesSymmetric or asymmetricRFC 7519
OAuth2 + OIDCHybrid (tokens + session)JWT ID/access tokensSSO, delegated auth, federationAsymmetric (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: issaudexpnbf.
  • For JWTs, avoid sensitive data in payload unless encrypted (JWE).
  • Rotate signing keys regularly, and use key IDs (kid header) 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+.

Post a Comment

Previous Post Next Post