ASP.NET Core Security Overview
Security is a critical aspect of any web application. ASP.NET Core provides a variety of tools and mechanisms to secure applications, including authentication and authorization.
- Authentication: The process of determining a user's identity.
- Authorization: The process of determining if an authenticated user has access to a resource.
We're going to go thru a few of the most relevant subjects for ASP.NET Core. Let's go thru a few things that I feel you need to know:
- What are JWTs?
- The
Authorize
attribute, claims, roles, and policies - Common authorization patterns for ASP.NET Core APIs
- A (not good) simple example of implementing JWT authentication. In fact it's more about showing off security features w/in ASP.NET Core than anything.
What are JWTs (JSON Web Tokens)
JWT is a compact, URL-safe means of representing claims to be transferred between two parties. JWTs are often used for authenticating users in ASP.NET Core applications.
- Header: Contains metadata about the token (e.g., type and signing algorithm).
- Payload: Contains the claims, which are statements about an entity (typically, the user) and additional data.
- Signature: Ensures that the token hasn’t been altered.
Example of JWT Structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The token is typically sent in the Authorization
header with the Bearer
prefix.
(Note, you can decode a JWT using a tool like jwt.io. This is surprisingly useful for debugging JWT related issues.)
Authorization: Bearer <token>
Claims
Claims represent key-value pairs associated with a user. They are often used in JWTs to define the roles, permissions, or other relevant information about the user.
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "John Doe"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim("Department", "Engineering")
};
Claims can be used in policies and for authorization decisions.
Authorize Attribute
The [Authorize]
attribute in ASP.NET Core restricts access to actions or controllers based on the user's authentication and authorization status.
Example:
[Authorize]
public class SecureController : Controller
{
public IActionResult Index()
{
return View();
}
}
You can also specify roles that the user must have:
[Authorize(Roles = "Administrator")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
Policies
In addition to role-based authorization, ASP.NET Core allows policy-based authorization. What is a policy? It's a collection of requirements that must be met for a user to access a resource. They're very flexible, and very powerful.
Policies are defined with requirements and handlers.
Example of Defining a Policy:
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Administrator"));
});
You can then apply the policy:
[Authorize(Policy = "AdminOnly")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
Policies are very powerful as they can be used to implement some fairly nuanced and complex authorization rules:
services.AddAuthorization(options =>
{
options.AddPolicy("SeniorEngineersFromEngineeringDepartmentOnly", policy =>
{
policy.RequireRole("SeniorEngineer");
policy.RequireClaim("Department", "Engineering");
});
});
🌶️🌶️🌶️ Authorization is extremely nuanced because it's dependent on what you're building. Most of the time, there's some customization that needs to be done to meet these requirements.
Common Authorization Patterns for ASP.NET Core APIs
Let's say you have a React app backed by ASP.NET Core. If I were starting a greenfield application, I'd probably:
- Create the React app using the dotnet CLI
- Install ASP.NET Core Identity
- Use cookie authentication to authenticate users
- Authorize access to the API endpoints using the [Authorize] attribute
It used to be common to request a bearer token, store it in local storage, and then use it to authenticate requests to the API. Cookies have emerged as the preferred method for authentication in modern web development due to their ease of use and built-in security features they are granted from the browser.
If I were writing an integration-level API, e.g. an API whose sole purpose is to provide a structured way to get and set data from my system, I'd probably implement some kind of API key scheme. API keys can be programmed to have a specific set of permissions (e.g. a specific scope of work, assuming you need that).
This is very common in many APIs, including Azure, Twilio, etc.
🌶️🌶️🌶️ Security is a big topic and you could do an entire course on it alone. The above is a very high level overview of some of the more common security mechanisms you might encounter. The sample implementation I demonstrated is TERRIBLE because it basically allows anyone to authenticate to our API.
Let's talk security in the example app
In the final version of the app, I added some basic security - essentially you must have a valid token to access any part of the API. But you can also easily get a token via the very insecure endpoint I created for the purposes of demonstration. Let's go through the code and see how it all works.