AUTH_SERVICES PowerUser Guide (C#)
Last updated: December 05, 2025
Author: Paul Namalomba
- SESKA Computational Engineer
- Software Developer
- PhD Candidate (Civil Engineering Spec. Computational and Applied Mechanics)
Contact: kabwenzenamalomba@gmail.com
Website: paulnamalomba.github.io
Overview
Enterprise authentication services provide centralized identity management, single sign-on (SSO), multi-factor authentication (MFA), and federated identity protocols (OAuth2, OIDC, SAML) for securing distributed systems at scale. This guide covers implementation patterns in C# for integrating with identity providers, token validation, claim-based authorization, SCIM provisioning, certificate-based authentication, and audit logging. Power users need to understand token lifecycle management, refresh strategies, key rotation, HSM/KeyVault integration, and high-availability architectures for production authentication infrastructure.
Contents
- AUTH_SERVICES PowerUser Guide (C#)
- Overview
- Contents
- Quickstart
- Key Concepts
- Configuration and Best Practices
- Security Considerations
- Examples
- Troubleshooting
- Performance and Tuning
- References and Further Reading
Quickstart
- Install packages:
dotnet add package Microsoft.Identity.Webanddotnet add package Microsoft.AspNetCore.Authentication.JwtBearer - Configure identity provider: Register application in Azure AD, Okta, Auth0, or Keycloak; obtain client ID, tenant ID, and client secret
- Add authentication middleware: Configure
AddMicrosoftIdentityWebApi()orAddJwtBearer()inProgram.cs - Protect endpoints: Apply
[Authorize]attribute to controllers or endpoints requiring authentication - Validate tokens: Use built-in token validation with issuer, audience, and signing key verification
- Extract claims: Access
User.Claimsin controllers for user identity, roles, and permissions
Key Concepts
- Identity Provider (IdP): Centralized service that authenticates users and issues tokens (Azure AD, Okta, Auth0, Keycloak, Ping Identity)
- OAuth 2.0: Authorization framework enabling third-party applications to obtain limited access to resources; uses authorization code, client credentials, refresh token flows
- OpenID Connect (OIDC): Identity layer on top of OAuth 2.0 providing authentication and user profile information via ID tokens
- SAML 2.0: XML-based protocol for exchanging authentication and authorization data between identity provider and service provider; common in legacy enterprise systems
- JWT (JSON Web Token): Compact, self-contained token format for securely transmitting claims between parties; consists of header, payload, and signature
- Claims: Key-value pairs representing user attributes (identity, roles, permissions); included in JWT payload and accessible in application code
- Token Lifetime: Time period during which token is valid; access tokens typically short-lived (5-60 min), refresh tokens long-lived (days/months)
- Refresh Token: Long-lived credential for obtaining new access tokens without re-authentication; must be stored securely and rotated periodically
- SCIM (System for Cross-domain Identity Management): RESTful protocol for automating user provisioning and de-provisioning across systems
- Token Revocation: Process of invalidating tokens before expiration; requires centralized token store or distributed cache for validation
- Certificate-Based Authentication: Uses X.509 certificates for mutual TLS authentication; common in B2B scenarios and high-security environments
Configuration and Best Practices
// Program.cs - ASP.NET Core minimal API with Azure AD authentication
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add authentication with Azure AD
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
// Add authorization policies
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"))
.AddPolicy("RequireMfa", policy => policy.RequireClaim("amr", "mfa"));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/secure", () => "Protected resource")
.RequireAuthorization();
app.MapGet("/api/admin", () => "Admin only resource")
.RequireAuthorization("AdminOnly");
app.Run();
// appsettings.json - Azure AD configuration
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"Audience": "api://your-api-id"
},
"Logging": {
"LogLevel": {
"Microsoft.Identity": "Information"
}
}
}
Best Practices:
- Use managed identities or KeyVault for client secrets; never hardcode credentials
- Implement token caching to reduce IdP calls; use IDistributedCache with Redis
- Validate token signature, issuer, audience, and expiration on every request
- Use short-lived access tokens (15-30 min) with refresh token rotation
- Implement proper CORS policies; avoid wildcard origins in production
- Log authentication failures with correlation IDs for security auditing
- Use HTTPS only; disable insecure protocols (TLS < 1.2)
- Implement rate limiting to prevent token endpoint abuse
- Store refresh tokens encrypted in database; rotate on each use
- Use PKCE (Proof Key for Code Exchange) for public clients (SPAs, mobile)
Security Considerations
Token Security:
// Custom token validation with additional security checks
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(2), // Reduce default 5 min skew
RequireExpirationTime = true,
RequireSignedTokens = true
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
// Check token revocation against distributed cache
var tokenId = context.Principal?.FindFirst("jti")?.Value;
var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
var revoked = await cache.GetStringAsync($"revoked:{tokenId}");
if (revoked != null)
{
context.Fail("Token has been revoked");
}
},
OnAuthenticationFailed = context =>
{
// Log authentication failures with security context
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogWarning(
"Authentication failed: {Exception} | IP: {IP} | Path: {Path}",
context.Exception.Message,
context.HttpContext.Connection.RemoteIpAddress,
context.HttpContext.Request.Path
);
return Task.CompletedTask;
}
};
});
Certificate-Based Authentication:
// Mutual TLS authentication with client certificates
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.Online;
options.ValidateCertificateUse = true;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
// Validate against trusted certificate store
var validationService = context.HttpContext.RequestServices
.GetRequiredService<ICertificateValidationService>();
if (!validationService.IsTrusted(context.ClientCertificate))
{
context.Fail("Certificate not trusted");
}
// Add certificate thumbprint as claim
var claims = new[]
{
new Claim("cert_thumbprint", context.ClientCertificate.Thumbprint),
new Claim("cert_subject", context.ClientCertificate.Subject)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
};
});
// Configure Kestrel for client certificates
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
https.ClientCertificateValidation = (cert, chain, errors) =>
{
// Custom validation logic
return errors == SslPolicyErrors.None;
};
});
});
Key Security Measures: - Use Azure KeyVault or AWS Secrets Manager for signing keys; rotate every 90 days - Implement key versioning with graceful rollover period (accept both old and new keys for 24 hours) - Store private keys in HSM (Hardware Security Module) for production environments - Use separate signing keys per environment (dev, staging, production) - Implement token binding to prevent token replay attacks - Use nonce values in authentication requests to prevent replay - Implement proper session management with secure, httponly, samesite cookies - Use content security policy (CSP) headers to prevent XSS attacks - Implement account lockout after failed login attempts (5 attempts, 15 min lockout) - Audit all authentication events with user context, IP, user-agent, timestamp
Examples
Example 1: OAuth 2.0 Authorization Code Flow with PKCE
// OAuth2 client implementation with PKCE
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
public class OAuth2Controller : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
private readonly IDistributedCache _cache;
public OAuth2Controller(IHttpClientFactory httpClientFactory,
IConfiguration config, IDistributedCache cache)
{
_httpClientFactory = httpClientFactory;
_config = config;
_cache = cache;
}
[HttpGet("login")]
public IActionResult Login()
{
// Generate PKCE code verifier and challenge
var codeVerifier = GenerateCodeVerifier();
var codeChallenge = GenerateCodeChallenge(codeVerifier);
var state = Guid.NewGuid().ToString("N");
// Store code verifier and state in cache
_cache.SetString($"verifier:{state}", codeVerifier,
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
// Build authorization URL
var authUrl = $"{_config["OAuth:AuthorizationEndpoint"]}" +
$"?response_type=code" +
$"&client_id={_config["OAuth:ClientId"]}" +
$"&redirect_uri={Uri.EscapeDataString(_config["OAuth:RedirectUri"])}" +
$"&scope={Uri.EscapeDataString("openid profile email")}" +
$"&state={state}" +
$"&code_challenge={codeChallenge}" +
$"&code_challenge_method=S256";
return Redirect(authUrl);
}
[HttpGet("callback")]
public async Task<IActionResult> Callback(string code, string state)
{
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
return BadRequest("Missing code or state");
// Retrieve and validate code verifier
var codeVerifier = await _cache.GetStringAsync($"verifier:{state}");
if (codeVerifier == null)
return BadRequest("Invalid state or expired session");
// Exchange authorization code for tokens
var client = _httpClientFactory.CreateClient();
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, _config["OAuth:TokenEndpoint"])
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["code"] = code,
["redirect_uri"] = _config["OAuth:RedirectUri"],
["client_id"] = _config["OAuth:ClientId"],
["code_verifier"] = codeVerifier
})
};
var response = await client.SendAsync(tokenRequest);
if (!response.IsSuccessStatusCode)
return StatusCode((int)response.StatusCode, "Token exchange failed");
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
// Store tokens securely
await StoreTokensSecurely(tokenResponse);
return Ok(new { message = "Authentication successful", tokenResponse.AccessToken });
}
private static string GenerateCodeVerifier()
{
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
private static string GenerateCodeChallenge(string codeVerifier)
{
var bytes = SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier));
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
private async Task StoreTokensSecurely(TokenResponse tokens)
{
// Encrypt and store refresh token in database
var userId = User.FindFirst("sub")?.Value;
// Implementation: Store encrypted refresh token with user ID
await Task.CompletedTask;
}
}
public record TokenResponse(
string AccessToken,
string RefreshToken,
int ExpiresIn,
string TokenType,
string IdToken
);
Example 2: Custom Claims Transformation and Policy-Based Authorization
// Claims transformation service
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
public class CustomClaimsTransformer : IClaimsTransformation
{
private readonly IUserRoleService _roleService;
public CustomClaimsTransformer(IUserRoleService roleService)
{
_roleService = roleService;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Clone current identity
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaims(principal.Claims);
// Get user ID from existing claims
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == null) return principal;
// Load roles from database and add as claims
var roles = await _roleService.GetUserRolesAsync(userId);
foreach (var role in roles)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
}
// Add custom claims from business logic
var permissions = await _roleService.GetUserPermissionsAsync(userId);
foreach (var permission in permissions)
{
claimsIdentity.AddClaim(new Claim("permission", permission));
}
return new ClaimsPrincipal(claimsIdentity);
}
}
// Register claims transformation
builder.Services.AddTransient<IClaimsTransformation, CustomClaimsTransformer>();
// Custom authorization requirement
using Microsoft.AspNetCore.Authorization;
public class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; }
public PermissionRequirement(string permission)
{
Permission = permission;
}
}
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
if (context.User.HasClaim("permission", requirement.Permission))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Register authorization policies
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanCreateUsers", policy =>
policy.Requirements.Add(new PermissionRequirement("users.create")))
.AddPolicy("CanDeleteUsers", policy =>
policy.Requirements.Add(new PermissionRequirement("users.delete")));
// Use in controllers
[Authorize(Policy = "CanCreateUsers")]
[HttpPost("users")]
public async Task<IActionResult> CreateUser([FromBody] UserDto user)
{
// Implementation
return Ok();
}
Example 3: SCIM 2.0 User Provisioning Implementation
// SCIM user provisioning controller
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("scim/v2")]
[Authorize(Roles = "SCIMClient")]
public class ScimUsersController : ControllerBase
{
private readonly IUserProvisioningService _provisioningService;
public ScimUsersController(IUserProvisioningService provisioningService)
{
_provisioningService = provisioningService;
}
[HttpGet("Users")]
public async Task<IActionResult> GetUsers(
[FromQuery] string filter,
[FromQuery] int startIndex = 1,
[FromQuery] int count = 100)
{
var users = await _provisioningService.GetUsersAsync(filter, startIndex, count);
return Ok(new
{
schemas = new[] { "urn:ietf:params:scim:api:messages:2.0:ListResponse" },
totalResults = users.TotalCount,
startIndex,
itemsPerPage = count,
Resources = users.Items
});
}
[HttpGet("Users/{id}")]
public async Task<IActionResult> GetUser(string id)
{
var user = await _provisioningService.GetUserByIdAsync(id);
if (user == null) return NotFound();
return Ok(user);
}
[HttpPost("Users")]
public async Task<IActionResult> CreateUser([FromBody] ScimUser user)
{
var created = await _provisioningService.CreateUserAsync(user);
return CreatedAtAction(nameof(GetUser), new { id = created.Id }, created);
}
[HttpPut("Users/{id}")]
public async Task<IActionResult> UpdateUser(string id, [FromBody] ScimUser user)
{
var updated = await _provisioningService.UpdateUserAsync(id, user);
if (updated == null) return NotFound();
return Ok(updated);
}
[HttpPatch("Users/{id}")]
public async Task<IActionResult> PatchUser(string id, [FromBody] ScimPatchRequest patch)
{
var updated = await _provisioningService.PatchUserAsync(id, patch);
if (updated == null) return NotFound();
return Ok(updated);
}
[HttpDelete("Users/{id}")]
public async Task<IActionResult> DeleteUser(string id)
{
var result = await _provisioningService.DeleteUserAsync(id);
if (!result) return NotFound();
return NoContent();
}
}
public record ScimUser(
string Id,
string UserName,
ScimName Name,
string[] Emails,
bool Active,
ScimMeta Meta
);
public record ScimName(string GivenName, string FamilyName);
public record ScimMeta(string ResourceType, DateTime Created, DateTime LastModified);
public record ScimPatchRequest(
string[] Schemas,
ScimPatchOperation[] Operations
);
public record ScimPatchOperation(string Op, string Path, object Value);
Example 4: Token Refresh and Rotation Strategy
// Token refresh service with rotation
public class TokenRefreshService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
private readonly ITokenStore _tokenStore;
private readonly ILogger<TokenRefreshService> _logger;
public TokenRefreshService(
IHttpClientFactory httpClientFactory,
IConfiguration config,
ITokenStore tokenStore,
ILogger<TokenRefreshService> logger)
{
_httpClientFactory = httpClientFactory;
_config = config;
_tokenStore = tokenStore;
_logger = logger;
}
public async Task<TokenResponse> RefreshAccessTokenAsync(string userId)
{
// Retrieve stored refresh token
var refreshToken = await _tokenStore.GetRefreshTokenAsync(userId);
if (refreshToken == null)
throw new InvalidOperationException("No refresh token found");
// Check if token is still valid
if (refreshToken.ExpiresAt < DateTime.UtcNow)
throw new InvalidOperationException("Refresh token expired");
// Exchange refresh token for new access token
var client = _httpClientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, _config["OAuth:TokenEndpoint"])
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["refresh_token"] = refreshToken.Token,
["client_id"] = _config["OAuth:ClientId"],
["client_secret"] = _config["OAuth:ClientSecret"]
})
};
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Token refresh failed for user {UserId}: {StatusCode}",
userId, response.StatusCode);
// If refresh token is invalid, revoke and force re-authentication
await _tokenStore.RevokeRefreshTokenAsync(userId);
throw new InvalidOperationException("Token refresh failed");
}
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
// Rotate refresh token (store new one, invalidate old one)
if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
{
await _tokenStore.StoreRefreshTokenAsync(userId, new RefreshTokenData
{
Token = tokenResponse.RefreshToken,
ExpiresAt = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
IssuedAt = DateTime.UtcNow
});
}
_logger.LogInformation("Token refreshed successfully for user {UserId}", userId);
return tokenResponse;
}
public async Task RevokeTokenAsync(string userId, string token)
{
// Revoke token at identity provider
var client = _httpClientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, _config["OAuth:RevocationEndpoint"])
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["token"] = token,
["token_type_hint"] = "refresh_token",
["client_id"] = _config["OAuth:ClientId"],
["client_secret"] = _config["OAuth:ClientSecret"]
})
};
await client.SendAsync(request);
// Remove from local store
await _tokenStore.RevokeRefreshTokenAsync(userId);
_logger.LogInformation("Token revoked for user {UserId}", userId);
}
}
public interface ITokenStore
{
Task<RefreshTokenData?> GetRefreshTokenAsync(string userId);
Task StoreRefreshTokenAsync(string userId, RefreshTokenData token);
Task RevokeRefreshTokenAsync(string userId);
}
public record RefreshTokenData
{
public string Token { get; init; }
public DateTime ExpiresAt { get; init; }
public DateTime IssuedAt { get; init; }
}
Troubleshooting
Token Validation Failures:
// Enhanced logging for token validation issues
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
// Log detailed token validation errors
if (context.Exception is SecurityTokenExpiredException)
{
logger.LogWarning("Token expired: {Message}", context.Exception.Message);
context.Response.Headers.Add("X-Token-Expired", "true");
}
else if (context.Exception is SecurityTokenInvalidSignatureException)
{
logger.LogError("Invalid token signature: {Message}", context.Exception.Message);
}
else if (context.Exception is SecurityTokenInvalidIssuerException)
{
logger.LogError("Invalid issuer: {Message}", context.Exception.Message);
}
else if (context.Exception is SecurityTokenInvalidAudienceException)
{
logger.LogError("Invalid audience: {Message}", context.Exception.Message);
}
else
{
logger.LogError(context.Exception, "Authentication failed");
}
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogDebug("Token received from {Source}",
context.Request.Headers.Authorization.FirstOrDefault()?.Substring(0, 20) ?? "none");
return Task.CompletedTask;
}
};
});
Common Issues:
- 401 Unauthorized: Check token expiration, signature validation, issuer/audience mismatch
- 403 Forbidden: User lacks required claims/roles; verify claims transformation pipeline
- Token refresh loop: Ensure refresh token rotation logic doesn't create circular dependencies
- CORS errors: Configure proper CORS policy with credentials support for cross-origin requests
- Clock skew: Adjust ClockSkew parameter in token validation; default 5 minutes may be too large
- Missing claims: Ensure IdP is configured to include required claims in tokens; check scope mappings
- Certificate validation failures: Verify certificate chain, CRL/OCSP endpoints, and trust store configuration
Diagnostic Commands:
# Decode JWT token (use jwt.io or jwt-cli)
dotnet tool install --global jwt-cli
jwt decode <token>
# Test token validation
curl -H "Authorization: Bearer <token>" https://api.example.com/validate
# Check certificate validity
openssl x509 -in certificate.pem -text -noout
openssl verify -CAfile ca-bundle.pem certificate.pem
Performance and Tuning
Token Caching Strategy:
// Implement distributed token cache with Redis
public class RedisTokenCache
{
private readonly IDistributedCache _cache;
private readonly ILogger<RedisTokenCache> _logger;
public RedisTokenCache(IDistributedCache cache, ILogger<RedisTokenCache> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<string?> GetAccessTokenAsync(string userId)
{
var cacheKey = $"access_token:{userId}";
var token = await _cache.GetStringAsync(cacheKey);
if (token != null)
{
_logger.LogDebug("Cache hit for user {UserId}", userId);
}
return token;
}
public async Task SetAccessTokenAsync(string userId, string token, int expiresIn)
{
var cacheKey = $"access_token:{userId}";
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(expiresIn - 60) // 60s buffer
};
await _cache.SetStringAsync(cacheKey, token, options);
_logger.LogDebug("Cached token for user {UserId} with expiry {ExpiresIn}s", userId, expiresIn);
}
}
// Use in application
public class SecureApiClient
{
private readonly HttpClient _httpClient;
private readonly RedisTokenCache _tokenCache;
private readonly ITokenService _tokenService;
public async Task<string> GetAccessTokenAsync(string userId)
{
// Try cache first
var cachedToken = await _tokenCache.GetAccessTokenAsync(userId);
if (cachedToken != null)
return cachedToken;
// Fetch new token and cache
var tokenResponse = await _tokenService.GetTokenAsync(userId);
await _tokenCache.SetAccessTokenAsync(userId, tokenResponse.AccessToken, tokenResponse.ExpiresIn);
return tokenResponse.AccessToken;
}
}
Performance Optimizations:
- Cache validated tokens in memory for duration of their lifetime; use MemoryCache for single-instance apps
- Use Redis or another distributed cache for multi-instance deployments to share token validation cache
- Implement connection pooling for HTTP clients calling IdP endpoints; use IHttpClientFactory
- Enable response compression for token endpoints returning large ID tokens
- Use async/await throughout authentication pipeline to prevent thread pool starvation
- Implement circuit breaker pattern for IdP calls to handle temporary outages gracefully
- Pre-fetch signing keys from JWKS endpoint on startup and cache with background refresh
- Use bulk operations for SCIM provisioning instead of individual API calls
- Implement pagination for large user/group queries (max 100-200 items per page)
- Monitor authentication latency and set alerts for p95 > 500ms
Monitoring Metrics:
// Custom metrics for authentication monitoring
using System.Diagnostics.Metrics;
public class AuthenticationMetrics
{
private readonly Meter _meter;
private readonly Counter<long> _authSuccessCounter;
private readonly Counter<long> _authFailureCounter;
private readonly Histogram<double> _tokenValidationDuration;
private readonly Histogram<double> _tokenRefreshDuration;
public AuthenticationMetrics(IMeterFactory meterFactory)
{
_meter = meterFactory.Create("Authentication");
_authSuccessCounter = _meter.CreateCounter<long>("auth.success", "count");
_authFailureCounter = _meter.CreateCounter<long>("auth.failure", "count");
_tokenValidationDuration = _meter.CreateHistogram<double>("auth.validation.duration", "ms");
_tokenRefreshDuration = _meter.CreateHistogram<double>("auth.refresh.duration", "ms");
}
public void RecordAuthSuccess(string provider)
=> _authSuccessCounter.Add(1, new KeyValuePair<string, object?>("provider", provider));
public void RecordAuthFailure(string provider, string reason)
=> _authFailureCounter.Add(1,
new KeyValuePair<string, object?>("provider", provider),
new KeyValuePair<string, object?>("reason", reason));
public void RecordTokenValidation(double milliseconds)
=> _tokenValidationDuration.Record(milliseconds);
public void RecordTokenRefresh(double milliseconds)
=> _tokenRefreshDuration.Record(milliseconds);
}
Scaling Considerations: - Deploy multiple instances behind load balancer with sticky sessions if using in-memory cache - Use Redis Cluster for distributed cache in high-throughput scenarios (>10K req/s) - Implement rate limiting per user/IP to prevent abuse (use AspNetCoreRateLimit library) - Consider dedicated authentication service/sidecar pattern for microservices architecture - Use connection multiplexing for HTTP/2 to IdP endpoints - Implement background token refresh for long-running processes to avoid synchronous delays - Monitor IdP rate limits and implement backoff/retry strategies
References and Further Reading
Official Documentation: - Microsoft Identity Platform - OAuth 2.0 RFC 6749 - OpenID Connect Core 1.0 - SAML 2.0 Technical Overview - SCIM 2.0 RFC 7643 - JWT RFC 7519 - PKCE RFC 7636
Libraries and Tools: - Microsoft.Identity.Web - Azure AD integration - IdentityServer - .NET OAuth/OIDC server - Auth0 .NET SDK - Auth0 integration - Okta .NET SDK - Okta integration - JWT.NET - JWT encoding/decoding - BouncyCastle - Cryptography library
Best Practices Guides: - OAuth 2.0 Security Best Current Practice - OWASP Authentication Cheat Sheet - NIST Digital Identity Guidelines - Microsoft Identity Platform Best Practices
Community Resources: - Auth0 Blog - Identity & Security - Identity Server Documentation - .NET Security on GitHub