Understanding Role-Based Access Control with ASP.NET Web APIs
Tags: dotnet, api, security, webdev
Published: 2023-06-06 10:10:09

When you're building an application and want to restrict access to resources and information for different kinds of users, **Role-Based Access Control (RBAC)** is one of the best, most structured models that you can implement 🔐

## What is Role-Based Access Control?

Role-Based Access Control (or RBAC) is a security model that restricts system access based on the roles assigned to users within an org. It provides a way to manage user permissions by associating users with specific roles and granting access to resources on their basis.

![A 3D diagram representing how RBAC works (image source: SuperTokens)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1m7kfc0ytsmik37c387b.jpeg)

For e.g., in a WhatsApp group chat, you have a normal user and an admin. While the user can access necessary services such as sending and reading messages, admins get extra tooling like creating invite links or adding/removing users. This is how RBAC works in essence.

## Implementing RBAC in ASP.NET Web APIs

Having spent a fair amount of time building web APIs with [.NET](https://dot.net), one of the easiest ways to **implement RBAC** that I discovered was by **adding roles as claims in JWTs**.

In case you aren't aware of what JWTs are, check out this Twitter thread I wrote last week:

{% twitter 1661369697521000448 %}

RBAC is implemented via JWTs in [ASP.NET](https://asp.net) through the following steps:

### Step 1️⃣: Define roles for your application

Identify the different roles that users can have in your system. Examples of roles could be "admin," "everyone," "moderator," etc.

### Step 2️⃣: Assign the roles

Associate specific roles with individual users. This can be stored in a database or any other persistent storage.

### Step 3️⃣: Generate JWT

When a user authenticates, generate a JWT that includes the user's roles as claims in the payload.

```csharp
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["JWT:SecretKey"]);

var tokenDescriptor = new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(new Claim[]
    {
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim(ClaimTypes.GivenName, user.Name),
        new Claim(ClaimTypes.Role, user.Role)
    }),
    IssuedAt = DateTime.UtcNow,
    Issuer = _configuration["JWT:Issuer"],
    Audience = _configuration["JWT:Audience"],
    Expires = DateTime.UtcNow.AddMinutes(30),
    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
};

var token = tokenHandler.CreateToken(tokenDescriptor);
```

The following NuGet packages are necessary for this purpose and must be imported at the top of your file via `using` directives, as shown:

```
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
```

### Step 4️⃣: Protect API Endpoints

In your ASP.NET Web API controllers or actions that require authorization, apply the `[Authorize]` attribute to specify that the endpoint requires authentication. These may include specific roles as well.

```csharp
// GET: auth/test
[Authorize(Roles = "Everyone")]
[HttpGet]
public IActionResult Test()
{
    .
    .
    .
```

### Step 5️⃣: Validate JWTs

Implement a JWT validation mechanism in your API to verify the authenticity and integrity of incoming JWTs. This may involve validating the issue, audience, signature, and token expiry.

```csharp
builder.Services.AddAuthentication(opt =>
{
    opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{   
    opt.RequireHttpsMetadata = false; // for development only
    opt.SaveToken = true;
    opt.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(builder.Configuration["JWT:SecretKey"])),
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["JWT:Issuer"],
        ValidateAudience = true,
        ValidAudience = builder.Configuration["JWT:Audience"]
    };
});
```

### Step 6️⃣: Extract Roles from JWT

Once the JWT is validated, you can extract the user's roles from the JWT claims.

### Step 7️⃣: Authorize Access

Finally, based on the roles extracted, implement the logic to allow or deny access to resources or actions within your API.

```csharp
// GET: auth/test
[Authorize(Roles = "Everyone")]
[HttpGet]
public IActionResult Test()
{
    string token = Request.Headers["Authorization"];

    if (token.StartsWith("Bearer"))
    {
        token = token.Substring("Bearer ".Length).Trim();
    }
    var handler = new JwtSecurityTokenHandler();

    JwtSecurityToken jwt = handler.ReadJwtToken(token);

    var claims = new Dictionary<string, string>();

    foreach (var claim in jwt.Claims)
    {
        claims.Add(claim.Type, claim.Value);
    }

    return Ok(claims);
}
```

If you'd like to see and try a very simplified sample of RBAC implemented via JWTs in .NET, check out this project:

{% github adityaoberai/JWTAuthSample %}

## Conclusion

To conclude, by implementing RBAC using JWTs, you can provide fine-grained access control to your resources based on the roles assigned to users, and allows for flexibility and scalability in managing access privileges within your application.