“In modern web applications, implementing robust authentication is crucial for security. However, different clients often require different authentication mechanisms. For example, your browser-based applications might use cookies, while your mobile apps or third-party integrations might use bearer tokens or API keys. This guide explores these authentication methods in depth, with special attention to API key authentication (which I also cover in my video tutorial on implementing API key authentication in .NET)
Fortunately, ASP.NET Core provides a flexible authentication system that allows you to implement and combine multiple authentication schemes. In this comprehensive guide, we’ll explore how to implement and use multiple authentication methods in your .NET applications.
Before diving into the implementation details, let’s understand what authentication schemes are in ASP.NET Core.
An authentication scheme is a named configuration that specifies:
ASP.NET Core comes with several built-in authentication handlers:
You can also create custom authentication handlers, such as for API key authentication.
Let’s look at some common scenarios where you might need multiple authentication methods:
Let’s implement a solution that supports three authentication methods:
First, let’s create a new ASP.NET Core Web API project and add the necessary packages:
dotnet new webapi -n Acme.MultiAuth
cd Acme.MultiAuth
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Let’s start by defining our principal model, which represents authenticated entities in our system:
public record Principal
{
public string Id { get; init; } = string.Empty;
public string Type { get; init; } = string.Empty;
public string Source { get; init; } = string.Empty;
public string OrganizationCode { get; init; } = string.Empty;
public IReadOnlyCollection<string> Roles { get; init; } = [];
public bool IsUser => Type == "usr";
public bool IsApplication => Type == "app";
public bool IsInternal => Source == "internal";
public bool IsExternal => Source == "external";
}
This model allows us to represent different types of authenticated entities (users or applications) from different sources (internal or external).
First, let’s configure JWT Bearer authentication:
public static class JwtBearerExtensions
{
public static AuthenticationBuilder AddJwtBearerAuthentication(
this AuthenticationBuilder builder,
IConfiguration configuration)
{
var jwtSettings = configuration.GetSection("JwtSettings");
var key = Encoding.ASCII.GetBytes(jwtSettings["Secret"] ??
throw new InvalidOperationException("JWT Secret not configured"));
builder.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var identity = context.Principal?.Identity as ClaimsIdentity;
if (identity == null) return Task.CompletedTask;
// Extract claims and create our Principal
var principalId = identity.FindFirst("acme_principal_id")?.Value;
var principalType = identity.FindFirst("acme_principal_type")?.Value;
var principalSource = identity.FindFirst("acme_principal_source")?.Value;
var orgCode = identity.FindFirst("acme_organization")?.Value;
if (principalId != null && principalType != null && principalSource != null)
{
var principal = new Principal
{
Id = principalId,
Type = principalType,
Source = principalSource,
OrganizationCode = orgCode ?? string.Empty,
Roles = identity.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList()
};
// Store the principal in the HttpContext for later use
context.HttpContext.Items["Principal"] = principal;
}
return Task.CompletedTask;
}
};
});
return builder;
}
}
Now, let’s implement API key authentication using a custom authentication handler:
public static class ApiKeyAuthenticationExtensions
{
public static AuthenticationBuilder AddApiKeyAuthentication(
this AuthenticationBuilder builder)
{
return builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationDefaults.AuthenticationScheme,
null);
}
}
public static class ApiKeyAuthenticationDefaults
{
public const string AuthenticationScheme = "ApiKey";
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string ApiKeyHeaderName = "X-API-Key";
private readonly IApiKeyService _apiKeyService;
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFprincipaly logger,
UrlEncoder encoder,
ISystemClock clock,
IApiKeyService apiKeyService)
: base(options, logger, encoder, clock)
{
_apiKeyService = apiKeyService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
{
return AuthenticateResult.NoResult();
}
var providedApiKey = apiKeyHeaderValues.FirstOrDefault();
if (string.IsNullOrWhiteSpace(providedApiKey))
{
return AuthenticateResult.NoResult();
}
var apiKey = await _apiKeyService.ValidateApiKeyAsync(providedApiKey);
if (apiKey == null)
{
return AuthenticateResult.Fail("Invalid API Key");
}
var claims = new List<Claim>
{
new Claim("acme_principal_id", apiKey.ApplicationId),
new Claim("acme_principal_type", "app"),
new Claim("acme_principal_source", "external"),
new Claim("acme_organization", apiKey.OrganizationCode)
};
foreach (var role in apiKey.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
// Store the principal in the HttpContext for later use
Context.Items["Principal"] = new Principal
{
Id = apiKey.ApplicationId,
Type = "app",
Source = "external",
OrganizationCode = apiKey.OrganizationCode,
Roles = apiKey.Roles
};
return AuthenticateResult.Success(ticket);
}
}
And here’s a simple implementation of the IApiKeyService
:
public interface IApiKeyService
{
Task<ApiKey?> ValidateApiKeyAsync(string providedApiKey);
}
public class ApiKey
{
public string Key { get; set; } = string.Empty;
public string ApplicationId { get; set; } = string.Empty;
public string OrganizationCode { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new();
}
public class ApiKeyService : IApiKeyService
{
private readonly Dictionary<string, ApiKey> _apiKeys;
public ApiKeyService(IConfiguration configuration)
{
// In a real application, you would store API keys in a database
// This is just a simple example
_apiKeys = new Dictionary<string, ApiKey>
{
{
"api-key-1",
new ApiKey
{
Key = "api-key-1",
ApplicationId = "app-1",
OrganizationCode = "org-1",
Roles = new List<string> { "api-read" }
}
},
{
"api-key-2",
new ApiKey
{
Key = "api-key-2",
ApplicationId = "app-2",
OrganizationCode = "org-2",
Roles = new List<string> { "api-read", "api-write" }
}
}
};
}
public Task<ApiKey?> ValidateApiKeyAsync(string providedApiKey)
{
_apiKeys.TryGetValue(providedApiKey, out var apiKey);
return Task.FromResult(apiKey);
}
}
Finally, let’s configure cookie authentication:
public static class CookieAuthenticationExtensions
{
public static AuthenticationBuilder AddCookieAuthentication(
this AuthenticationBuilder builder)
{
builder.AddCookie(options =>
{
options.Cookie.Name = "Acme.Auth";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
options.LoginPath = "/account/login";
options.LogoutPath = "/account/logout";
options.AccessDeniedPath = "/account/access-denied";
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var identity = context.Principal?.Identity as ClaimsIdentity;
if (identity == null) return Task.CompletedTask;
// Extract claims and create our Principal
var principalId = identity.FindFirst("acme_principal_id")?.Value;
var principalType = identity.FindFirst("acme_principal_type")?.Value;
var principalSource = identity.FindFirst("acme_principal_source")?.Value;
var orgCode = identity.FindFirst("acme_organization")?.Value;
if (principalId != null && principalType != null && principalSource != null)
{
var principal = new Principal
{
Id = principalId,
Type = principalType,
Source = principalSource,
OrganizationCode = orgCode ?? string.Empty,
Roles = identity.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList()
};
// Store the principal in the HttpContext for later use
context.HttpContext.Items["Principal"] = principal;
}
return Task.CompletedTask;
}
};
});
return builder;
}
}
Now, let’s configure our application to use all three authentication methods:
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register the API key service
builder.Services.AddSingleton<IApiKeyService, ApiKeyService>();
// Configure authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "MultiScheme";
options.DefaultChallengeScheme = "MultiScheme";
})
.AddJwtBearerAuthentication(builder.Configuration)
.AddApiKeyAuthentication()
.AddCookieAuthentication()
.AddPolicyScheme("MultiScheme", "Cookie or JWT or ApiKey", options =>
{
options.ForwardDefaultSelector = context =>
{
// Check if the request has an API key header
if (context.Request.Headers.ContainsKey("X-API-Key"))
{
return ApiKeyAuthenticationDefaults.AuthenticationScheme;
}
// Check if the request has a bearer token
if (context.Request.Headers.ContainsKey("Authorization") &&
context.Request.Headers["Authorization"].ToString().StartsWith("Bearer "))
{
return JwtBearerDefaults.AuthenticationScheme;
}
// Default to cookie authentication
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
// Configure authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("admin"));
options.AddPolicy("RequireApiWriteRole", policy =>
policy.RequireRole("api-write"));
options.AddPolicy("RequireUserOnly", policy =>
policy.RequireAssertion(context =>
{
if (context.Resource is HttpContext httpContext &&
httpContext.Items["Principal"] is Principal principal)
{
return principal.IsUser;
}
return false;
}));
options.AddPolicy("RequireApplicationOnly", policy =>
policy.RequireAssertion(context =>
{
if (context.Resource is HttpContext httpContext &&
httpContext.Items["Principal"] is Principal principal)
{
return principal.IsApplication;
}
return false;
}));
options.AddPolicy("RequireInternalOnly", policy =>
policy.RequireAssertion(context =>
{
if (context.Resource is HttpContext httpContext &&
httpContext.Items["Principal"] is Principal principal)
{
return principal.IsInternal;
}
return false;
}));
});
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
Now let’s create some controllers that use different authentication methods:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
[Authorize] // This will work with any authentication method
public IActionResult GetAll()
{
var principal = HttpContext.Items["Principal"] as Principal;
return Ok(new { Message = $"Hello, {principal?.Type} {principal?.Id} from {principal?.Source}" });
}
[HttpGet("admin")]
[Authorize(Policy = "RequireAdminRole")] // This requires the admin role
public IActionResult GetAdmin()
{
return Ok(new { Message = "Admin content" });
}
[HttpGet("users-only")]
[Authorize(Policy = "RequireUserOnly")] // This requires the principal to be a user
public IActionResult GetUsersOnly()
{
return Ok(new { Message = "User-only content" });
}
[HttpGet("apps-only")]
[Authorize(Policy = "RequireApplicationOnly")] // This requires the principal to be an application
public IActionResult GetAppsOnly()
{
return Ok(new { Message = "Application-only content" });
}
[HttpGet("internal-only")]
[Authorize(Policy = "RequireInternalOnly")] // This requires the principal to be internal
public IActionResult GetInternalOnly()
{
return Ok(new { Message = "Internal-only content" });
}
[HttpGet("api-write")]
[Authorize(Policy = "RequireApiWriteRole")] // This requires the api-write role
public IActionResult GetApiWrite()
{
return Ok(new { Message = "API write content" });
}
}
You can also specify which authentication scheme to use for a specific endpoint:
[ApiController]
[Route("api/[controller]")]
public class ApiOnlyController : ControllerBase
{
[HttpGet]
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Get()
{
return Ok(new { Message = "This endpoint only accepts API key authentication" });
}
}
[ApiController]
[Route("api/[controller]")]
public class JwtOnlyController : ControllerBase
{
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Get()
{
return Ok(new { Message = "This endpoint only accepts JWT bearer authentication" });
}
}
[ApiController]
[Route("api/[controller]")]
public class WebOnlyController : ControllerBase
{
[HttpGet]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Get()
{
return Ok(new { Message = "This endpoint only accepts cookie authentication" });
}
}
[ApiController]
[Route("api/[controller]")]
public class MultipleSchemeController : ControllerBase
{
[HttpGet]
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{ApiKeyAuthenticationDefaults.AuthenticationScheme}")]
public IActionResult Get()
{
return Ok(new { Message = "This endpoint accepts both JWT and API key authentication" });
}
}
Let’s look at a real-world example of how Acme Corporation uses multiple authentication methods for their payment processing system:
Acme Corporation has built a payment processing system with the following components:
For this system, Acme uses the following authentication methods:
Here’s how Acme implements this strategy:
// Configure authentication in Program.cs
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "MultiScheme";
})
.AddCookie("Cookies", options =>
{
options.Cookie.Name = "Acme.Dashboard";
options.LoginPath = "/account/login";
})
.AddJwtBearer("Bearer", options =>
{
// JWT configuration for mobile app
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", options => { })
.AddPolicyScheme("MultiScheme", "Cookie or JWT or ApiKey", options =>
{
options.ForwardDefaultSelector = context =>
{
// Select the appropriate scheme based on the request
if (context.Request.Path.StartsWithSegments("/api/partner"))
{
return "ApiKey";
}
if (context.Request.Headers.ContainsKey("Authorization") &&
context.Request.Headers["Authorization"].ToString().StartsWith("Bearer "))
{
return "Bearer";
}
return "Cookies";
};
});
// Configure authorization
builder.Services.AddAuthorization(options =>
{
// Employee policies
options.AddPolicy("RequireEmployeeRole", policy =>
policy.RequireRole("employee"));
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("admin"));
// Customer policies
options.AddPolicy("RequireCustomerRole", policy =>
policy.RequireRole("customer"));
// Partner policies
options.AddPolicy("RequirePartnerRole", policy =>
policy.RequireRole("partner"));
options.AddPolicy("RequirePremiumPartnerRole", policy =>
policy.RequireRole("premium-partner"));
});
[ApiController]
[Route("api/dashboard")]
[Authorize(AuthenticationSchemes = "Cookies", Policy = "RequireEmployeeRole")]
public class DashboardController : ControllerBase
{
[HttpGet("transactions")]
public IActionResult GetTransactions()
{
return Ok(new { Message = "Employee dashboard transactions" });
}
[HttpGet("reports")]
[Authorize(Policy = "RequireAdminRole")]
public IActionResult GetReports()
{
return Ok(new { Message = "Admin-only reports" });
}
}
[ApiController]
[Route("api/mobile")]
[Authorize(AuthenticationSchemes = "Bearer", Policy = "RequireCustomerRole")]
public class MobileController : ControllerBase
{
[HttpGet("transactions")]
public IActionResult GetTransactions()
{
return Ok(new { Message = "Customer mobile transactions" });
}
[HttpPost("payment")]
public IActionResult MakePayment()
{
return Ok(new { Message = "Payment processed" });
}
}
[ApiController]
[Route("api/partner")]
[Authorize(AuthenticationSchemes = "ApiKey", Policy = "RequirePartnerRole")]
public class PartnerController : ControllerBase
{
[HttpPost("process-payment")]
public IActionResult ProcessPayment()
{
return Ok(new { Message = "Partner payment processed" });
}
[HttpGet("transaction-status")]
public IActionResult GetTransactionStatus(string transactionId)
{
return Ok(new { Message = $"Status for transaction {transactionId}" });
}
[HttpGet("analytics")]
[Authorize(Policy = "RequirePremiumPartnerRole")]
public IActionResult GetAnalytics()
{
return Ok(new { Message = "Premium partner analytics" });
}
}
When implementing multiple authentication methods, consider the following best practices:
Choose meaningful names for your authentication schemes and be consistent throughout your application.
Customize authentication failure responses to provide helpful error messages without revealing sensitive information.
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
context.HandleResponse();
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
error = "Unauthorized",
message = "You are not authorized to access this resource"
});
return context.Response.WriteAsync(result);
}
};
Instead of hardcoding roles in your controllers, use policy-based authorization to make your code more maintainable.
Store API keys, JWT secrets, and other credentials securely using environment variables, Azure Key Vault, or another secure storage mechanism.
Protect your API from abuse by implementing rate limiting, especially for API key authentication.
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
if (context.User.Identity?.AuthenticationType == ApiKeyAuthenticationDefaults.AuthenticationScheme)
{
// Apply stricter rate limiting for API key authentication
return RateLimitPartition.GetFixedWindowLimiter(
context.User.Identity.Name ?? "anonymous",
_ => new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 60,
QueueLimit = 0
});
}
// Default rate limiting
return RateLimitPartition.GetFixedWindowLimiter(
context.User.Identity?.Name ?? "anonymous",
_ => new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 100,
QueueLimit = 0
});
});
});
For JWT and API key authentication, implement a mechanism to revoke tokens or keys when needed.
Always use HTTPS in production to encrypt authentication credentials in transit.
Problem: Setting the wrong default authentication scheme can lead to unexpected authentication failures.
Solution: Use a policy scheme as the default and explicitly forward to the appropriate scheme based on the request.
Problem: Forgetting to add the authentication middleware to the pipeline.
Solution: Always ensure you have app.UseAuthentication()
before app.UseAuthorization()
in your middleware pipeline.
Problem: The order of authentication schemes matters when using multiple schemes.
Solution: Configure your schemes in the correct order, with the most specific schemes first.
Problem: Default authentication failure responses may not be user-friendly or secure.
Solution: Customize authentication failure responses using the events provided by each authentication handler.
Problem: Using only one authentication method may not meet all your application’s needs.
Solution: Use multiple authentication methods as appropriate for different clients and scenarios.
Implementing multiple authentication methods in .NET is a powerful way to secure your application while providing flexibility for different clients. By using the techniques described in this guide, you can create a robust authentication system that meets the needs of your users, partners, and internal systems.
Remember that authentication is just one part of a comprehensive security strategy. Always follow security best practices, keep your dependencies updated, and stay informed about the latest security developments in the .NET ecosystem.
For more information on API key authentication specifically, you can check out my video tutorial on implementing API key authentication in .NET.