Comprehensive Documentation
Securing your Web API is crucial for protecting sensitive data and controlling access to your resources. ASP.NET Web API provides robust mechanisms for implementing both authentication and authorization.
ASP.NET Web API supports various authentication schemes:
A simple scheme where credentials (username and password) are sent in the HTTP Authorization
header, encoded in Base64. While easy to implement, it's not secure over unencrypted HTTP.
// In Startup.cs (or App_Start/Startup.cs for older projects)
public void ConfigureAuth(IAppBuilder app)
{
app.UseBasicAuthentication("MyRealm");
}
// Controller attribute
[Authorize] // This alone doesn't enable Basic Auth, it needs the UseBasicAuthentication middleware
public class MyController : ApiController
{
// ...
}
A more secure and scalable approach. The client authenticates once (e.g., with username/password) and receives a token. This token is then included in subsequent requests to prove identity. JSON Web Tokens (JWT) are a popular choice.
Key components:
You typically implement this using libraries like IdentityServer or by manually creating and validating JWTs with libraries like Microsoft.IdentityModel.Tokens.Jwt.
Industry-standard protocols for delegated authorization and authentication. They allow users to grant third-party applications access to their data without sharing their credentials directly.
ASP.NET Web API integrates well with OAuth 2.0 providers like Azure Active Directory, Auth0, or your own custom implementation.
A simpler form where a unique key is generated for each client. This key is sent in a request header or query parameter. Suitable for internal services or scenarios where clients are trusted.
// Example middleware for API Key validation
public class ApiKeyAuthAttribute : Attribute, IAuthenticationFilter
{
public async Task ChallengeAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid API Key", context.Request);
return Task.FromResult(0);
}
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
string apiKey = null;
// Try to get the key from header or query string
if (context.Request.Headers.Contains("X-API-Key"))
{
apiKey = context.Request.Headers.GetValues("X-API-Key").FirstOrDefault();
}
else
{
var query = context.Request.GetQueryNameValuePairs().FirstOrDefault(pair => pair.Key.Equals("apiKey", StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(query.Value))
{
apiKey = query.Value;
}
}
if (string.IsNullOrEmpty(apiKey))
{
context.ErrorResult = new AuthenticationFailureResult("API Key not provided", context.Request);
return;
}
// In a real application, you would validate the apiKey against a database or configuration
if (ValidateApiKey(apiKey))
{
var principal = new GenericPrincipal(new GenericIdentity("ApiKeyUser"), null); // Assign roles if needed
context.Principal = principal;
}
else
{
context.ErrorResult = new AuthenticationFailureResult("Invalid API Key", context.Request);
}
}
private bool ValidateApiKey(string key)
{
// Replace with your actual API key validation logic
return key == "your-secret-api-key";
}
}
// Controller attribute
[RoutePrefix("api/items")]
public class ItemsController : ApiController
{
[HttpGet]
[Route("")]
[ApiKeyAuth] // Apply the custom authentication filter
public IHttpActionResult GetAllItems()
{
return Ok(new[] { "Item 1", "Item 2", "Item 3" });
}
}
// Helper class for response
public class AuthenticationFailureResult : IHttpActionResult
{
private readonly string _reasonPhrase;
private readonly HttpRequestMessage _request;
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
_reasonPhrase = reasonPhrase;
_request = request;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
response.RequestMessage = _request;
response.ReasonPhrase = _reasonPhrase;
return response;
}
}
Once a user is authenticated, you need to control what they can access. ASP.NET Web API offers flexible authorization options.
Users are assigned to roles (e.g., "Admin", "User"). You can then restrict access to controllers or actions based on these roles.
[Authorize(Roles = "Admin")]
public class AdminController : ApiController
{
// Only users in the "Admin" role can access this controller
}
public class ReportsController : ApiController
{
[HttpGet]
[Authorize(Roles = "Manager,Admin")]
public IHttpActionResult GetSalesReports()
{
// Users in "Manager" or "Admin" roles can access this action
return Ok();
}
[HttpGet]
[Authorize(Roles = "User")]
public IHttpActionResult GetUserDashboard()
{
// Only users in the "User" role can access this action
return Ok();
}
}
A more flexible approach where authorization is based on policies. A policy can combine requirements like roles, claims, or other custom conditions.
This is often implemented using the Microsoft.AspNetCore.Authorization package (though the concepts apply to ASP.NET Web API as well, often requiring custom implementations or integration with Identity frameworks).
Example Scenario: A policy might require a user to be in the "Editor" role AND have a "CanEditContent" claim.
For complex authorization logic that doesn't fit standard roles or policies, you can create custom attributes inheriting from AuthorizeAttribute
or implementing IAuthorizationFilter
.
using System.Linq;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
public class OwnResourceAttribute : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
// Get the authenticated user's identity
IPrincipal principal = actionContext.RequestContext.Principal;
if (principal == null || !principal.Identity.IsAuthenticated)
{
base.OnAuthorization(actionContext); // Let the base class handle unauthorized
return;
}
// Assume the resource ID is passed as a route parameter named "id"
string resourceId = null;
if (actionContext.ActionArguments.ContainsKey("id"))
{
resourceId = actionContext.ActionArguments["id"]?.ToString();
}
else if (actionContext.ControllerContext.RouteData.Values.ContainsKey("id"))
{
resourceId = actionContext.ControllerContext.RouteData.Values["id"]?.ToString();
}
if (string.IsNullOrEmpty(resourceId))
{
// Cannot determine resource, maybe deny access or log an error
actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { ReasonPhrase = "Resource ID not found" };
return;
}
// In a real app, you'd check if the current user 'owns' this resourceId
// For demonstration, let's assume user "User_123" can only access resource "123"
bool canAccess = false;
if (principal.Identity.Name == "User_123" && resourceId == "123")
{
canAccess = true;
}
else if (principal.IsInRole("Admin")) // Admins can access anything
{
canAccess = true;
}
if (!canAccess)
{
actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { ReasonPhrase = "User does not have permission to access this resource." };
}
}
}
// Controller action using the custom attribute
[RoutePrefix("api/documents")]
public class DocumentsController : ApiController
{
[HttpGet]
[Route("{id}")]
[OwnResource] // Apply the custom authorization attribute
public IHttpActionResult GetDocument(string id)
{
// Logic to retrieve and return document with the specified ID
return Ok($"Document details for ID: {id}");
}
}