---
title: "Fine-Grained Authorization in ASP.NET Core with Auth0 FGA"
description: "Learn how to implement fine-grained, relationship-based authorization in an ASP.NET Core minimal API using Auth0 FGA."
authors:
  - name: "Andrea Chiarelli"
    url: "https://auth0.com/blog/authors/andrea-chiarelli/"
date: "Sep 5, 2025"
category: "Developers,Tutorial,.NET"
tags: ["authorization", "fga", "dotnet", "aspnet-core", "openfga"]
url: "https://auth0.com/blog/fine-grained-authorization-in-aspnet-core-with-auth0-fga/"
---

# Fine-Grained Authorization in ASP.NET Core with Auth0 FGA

Modern applications often require more sophisticated authorization logic than simple Role-Based Access Control (RBAC) can offer. Consider a collaborative application like Google Drive: a user's ability to view, edit, or delete a file isn't determined by a static role like *editor* but by their specific relationship to that individual file. Are they the owner? Have they been granted editing access? This is the domain of **Fine-Grained Authorization (FGA)**.

This article will guide you through enhancing a standard ASP.NET Core minimal API for file and folder management with a powerful Fine-Grained Authorization system, [Auth0 FGA](https://a0.to/auth0-fga-content). We will start with a basic API that uses standard JWT-based authorization and evolve it to enforce relationship-based permissions. By the end, you will learn how to model your authorization rules and use the OpenFGA SDK to check permissions, ensuring that only users with the correct relationship—like being the owner—can modify or delete a resource.

## Prerequisites

This article will focus on using Auth0 FGA with ASP.NET Core, so make sure you have:

* [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) or later installed on your machine.

* An IDE like **Visual Studio 2022** or **VS Code**, otherwise, if you are brave enough, you can simply use the **.NET CLI** and your favorite editor.  
* A running **Auth0 FGA instance**. You can follow the [Auth0 FGA getting started guide](https://docs.fga.dev/getting-started) to set an online instance or use a [Docker local instance of OpenFGA](https://openfga.dev/#quick-start), the open source engine of Auth0 FGA. In this article, we will focus on Auth0 FGA, but the concepts are easily adaptable to a local OpenFGA instance.

* A tool for making API requests, such as **Postman** or the `curl` command-line tool.  
* Familiarity with [ASP.NET Core minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) and basic authorization concepts.

> Curious about the differences between Auth0 FGA and OpenFGA? Check out [this document](https://docs.fga.dev/openfga-vs-auth0-fga).

<include src="Auth0TemplatesForDotnetCTA" />

## A Simple File Management API

The sample project you will be working on is a simple file and folder management API implemented with ASP.NET Core minimal API. This API manages two types of resources: folders and files. It exposes basic CRUD endpoints and protects them using a very simple authorization policy that just requires an authenticated user.

The behavior of the application is all defined within the `Program.cs` file, whose relevant parts you can see in the following code block:

```c#
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

// --- In-memory data stores for simplicity ---
var folders = new Dictionary<string, Folder>();
var files = new Dictionary<string, File>();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();

var documentsApi = app.MapGroup("/documents").RequireAuthorization();

// --- Folder Endpoints ---
documentsApi.MapPost("/folders", (CreateFolderRequest req, HttpContext context) =>
{
  var newFolder = new Folder(Guid.NewGuid().ToString(), req.Name, GetUserId(context));
  folders[newFolder.Id] = newFolder;
  return Results.Created($"/documents/folders/{newFolder.Id}", newFolder);
});

documentsApi.MapGet("/folders/{id}", (string id) =>
    folders.TryGetValue(id, out var folder) ? Results.Ok(folder) : Results.NotFound());

documentsApi.MapPut("/folders/{id}", (string id, UpdateFolderRequest req, HttpContext context) =>
{
  if (!folders.ContainsKey(id)) return Results.NotFound();

  var updatedFolder = folders[id] with { Name = req.Name };
  folders[id] = updatedFolder;
  return Results.Ok(updatedFolder);
});

documentsApi.MapDelete("/folders/{id}", (string id) =>
{
  return folders.Remove(id) ? Results.NoContent() : Results.NotFound();
});

// --- File Endpoints (similar structure) ---
documentsApi.MapPost("/files", (CreateFileRequest req, HttpContext context) =>
{
  var newFile = new File(Guid.NewGuid().ToString(), req.Name, GetUserId(context));
  files[newFile.Id] = newFile;
  return Results.Created($"/documents/files/{newFile.Id}", newFile);
});

documentsApi.MapGet("/files/{id}", (string id) =>
    files.TryGetValue(id, out var file) ? Results.Ok(file) : Results.NotFound());

// ... Other file endpoints (PUT, DELETE) with the same authorization issue

app.Run();

// --- Helper and Record Definitions ---
string GetUserId(HttpContext context) =>
    context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "unknown";

internal record Folder(string Id, string Name, string OwnerId);
internal record File(string Id, string Name, string OwnerId);
internal record CreateFolderRequest(string Name);
internal record UpdateFolderRequest(string Name);
internal record CreateFileRequest(string Name);

```

As you can see, folders and files are stored in memory for simplicity. All the endpoints are protected by the basic `RequireAuthorization()` policy, which simply requires that all the HTTP requests have a valid access token.

When a file or folder is created, the user ID is extracted from the access token via the `GetUserId()` function and is attached to that file or folder.

The configuration settings that instruct the API how to connect with Auth0 to validate the received access token are located in the `appsettings.json` file:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "Authority": "https://<YOUR_AUTH0_DOMAIN>",
        "ValidAudiences": [ "<YOUR_AUDIENCE>" ],
        "ValidIssuer": "<YOUR_AUTH0_DOMAIN>"
      }
    }
  }
}
```

Replace the placeholders `<YOUR_AUTH0_DOMAIN>` and `<YOUR_AUDIENCE>` with the respective values coming from [the registration of the API in your Auth0 dashboard](https://auth0.com/docs/get-started/auth0-overview/set-up-apis).

## The Authorization Challenge

This API works, but it has a significant flaw: **any authenticated user can update or delete any folder or file**. The default policy enforced by `RequireAuthorization()` only checks for a valid token.

Of course, this solution is not the best one from the authorization standpoint for several reasons.

First, it does not apply the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege). The current authorization policy allows any user to do anything on any resource. The only requirement is to be authenticated and have a valid access token. This scenario is rarely applicable in a real-world context. If a user doesn't need to delete a file, they should not have that permission. You should [restrict what users can do based on their permissions](https://auth0.com/blog/permission-based-security-aspnet-webapi) and [apply the principle of least privilege](https://auth0.com/blog/oauth2-access-tokens-and-principle-of-least-privilege/).

But even if you verify what a user can do based on the permission carried by the access token, you can still be exposed to [security risks such as Broken Object Level Authorization](https://auth0.com/blog/how-fine-grained-authorization-solves-critical-api-security-risk/) if this verification is not done correctly. The likelihood of running into such a risk increases even more when a scenario becomes complex and dynamic. In a typical file and folder management system, users who create resources have full permissions, but other users only have the permissions they're granted, often unevenly. In these scenarios, simply analyzing the permissions included in the access token isn't enough. This is where fine-grained authorization comes in.

In our specific case, we want to enforce a simple but powerful rule: **only the creator (the owner) of a folder or file can update or delete it**. This is a classic relationship-based authorization problem that standard Role-Based Access Control struggles to solve efficiently. Roles tell you what permissions a user has, but not on what specific resource. The *editor* role would allow a user to update and delete files and folders, unless you define a more specific role, such as *fileEditor* and *folderEditor*. But even in this case, you don’t have the granularity of controlling what specific file the user can update or not. And creating even more specific roles bound to specific files makes no sense. You need a system that can understand and check relationships like `user:anne` is the `owner` of `folder:project-alpha`.

This is where Auth0 FGA comes in.

## Introducing Auth0 FGA

[Auth0 FGA](https://docs.fga.dev/) is a fine-grained authorization service based on the [ReBAC model](https://docs.fga.dev/authorization-concepts#what-is-relationship-based-access-control) and  inspired by [Google's Zanzibar paper](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/). You will use this service to address the authorization issues we highlighted earlier. You will integrate Auth0 FGA in your application following these three steps:

1. **Define an Authorization Model**: Using a simple, human-readable language, you define the types of objects in your system (e.g., `user`, `folder`), the possible relationships between them (`owner`, `viewer`), and the rules for who can do what (e.g., "a user can edit a folder if they are an owner of that folder").  
2. **Store Relationship Data (Tuples)**: You tell Auth0 FGA about the specific relationships that exist in your application, such as `user:anne` is the `owner` of `folder:project-alpha`. These are called "tuples."  
3. **Check Permissions**: At runtime, your application queries Auth0 FGA with a simple question: "Can `user:anne` delete `folder:project-alpha`?" Auth0 FGA provides a fast yes/no answer based on your model and stored tuples.  
   

## Modeling Your Authorization Logic in Auth0 FGA

First, you need to create an authorization model that describes our kind of file system. Based on the [Google Drive use case documentation](https://docs.fga.dev/modeling/advanced/use-cases/gdrive), our model will define `user`, `folder`, and `file` types and the relations between them.

Using your [Auth0 FGA dashboard](https://dashboard.fga.dev/), add the following authorization model to your Auth0 FGA store:

```yaml
model
  schema 1.1

type user

type file
  relations
    define owner: [user]
    define can_view: owner
    define can_edit: owner
    define can_delete: owner

type folder
  relations
    define owner: [user]
    define parent: [folder]
    define can_view: owner or can_view from parent
    define can_edit: owner or can_edit from parent
    define can_delete: owner
```

Let's break this down:

* `type user`: A simple type representing your users.
* `type file`: Represents a file. It has one direct relationship, owner, which must be a user. The permissions `can_view`, `can_edit`, and `can_delete` are all defined as being equivalent to the `owner`.
* `type folder`: Represents a folder.
  * `owner: [user]`: A folder has an owner, who is a user.
  * `parent:[folder]`: A folder can be inside another folder.
  * `can_view: owner or can_view from parent`: This is a powerful rule! It means a user can view a folder if they are its direct owner **OR** if they have `can_view` permission on its parent folder (permission inheritance). 
  * `can_delete: owner`: Deleting a folder is restricted to its direct owner.

In summary, for simplicity, we have defined an authorization model where a file or folder can only be managed by its owner, who is then the creator of the file or folder. You can modify this model to address more complex scenarios, as you can see in the [Google Drive use case documentation](https://docs.fga.dev/modeling/advanced/use-cases/gdrive).

## Integrating Auth0 FGA into the ASP.NET Core API

Now, let's modify our C\# code to use this authorization model. The steps we are going to implement are:

1. **Add and configure the OpenFGA SDK.** While you can directly call the [Auth0 FGA API](https://docs.fga.dev/api/service), using an SDK simplifies your development experience. 
2. **Populate your store with relationship data**. When a file or folder is created, write a tuple to Auth0 FGA to store the ownership relationship.
3. **Check permissions**. Before updating or deleting a resource, check the permission with Auth0 FGA.

Once these three steps are done, your application will use the power of Auth0 FGA to perform its authorization checks.

### Set Up the OpenFGA SDK

First, add the [OpenFGA .NET SDK](https://github.com/openfga/dotnet-sdk) to your project by running the following command:

```shell
dotnet add package OpenFga.Sdk
```

Now, go to the *Settings* section of your [Auth0 FGA](https://dashboard.fga.dev/) dashboard and create a client by following [the instructions described here](https://docs.fga.dev/integration/getting-your-api-keys). You will be asked for a name for the client and the permissions you want to assign to it. For our purpose, select *Write and delete tuples* and *Read and query* permissions.

Once the client has been successfully created, a window with the client’s credentials will appear. Take note of the values of store ID, client ID, and client secret. Also, [based on the location chosen for your Auth0 FGA instance](https://docs.fga.dev/integration/getting-your-api-keys#03-setting-relevant-environment-variables), you will need the API URL, API audience, and API token issuer.

Add a new section named `Auth0Fga` to the `appsettings.json` file and store these values as shown below:

```json
{
  //...existing sections...

  "Auth0Fga": {
    "ApiUrl": "<YOUR_API_URL>",
    "StoreId": "<YOUR_STORE_ID>",
    "ApiTokenIssuer": "<YOURAPI_TOKEN_ISSUER>",
    "ApiAudience": "<YOUR_API_AUDIENCE>",
    "ClientId": "<YOUR_CLIENT_ID>",
    "ClientSecret": "<YOUR_CLIENT_SECRET>"
  }
}
```

Next, configure the `OpenFgaClient` in `Program.cs`. This client will handle all communication with your Auth0 FGA instance:

```c#
//...existing using clauses...
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Client.Model;
using OpenFga.Sdk.Configuration;

//...existing code...

//👇 new code

builder.Services.AddSingleton(sp =>
{
  var configuration = new ClientConfiguration()
  {
    ApiUrl = builder.Configuration["Auth0Fga:ApiUrl"],
    StoreId = builder.Configuration["Auth0Fga:StoreId"],
    Credentials = new Credentials()
    {
      Method = CredentialsMethod.ClientCredentials,
      Config = new CredentialsConfig()
      {
        ApiTokenIssuer = builder.Configuration["Auth0Fga:ApiTokenIssuer"],
        ApiAudience = builder.Configuration["Auth0Fga:ApiAudience"],
        ClientId = builder.Configuration["Auth0Fga:ClientId"],
        ClientSecret = builder.Configuration["Auth0Fga:ClientSecret"],
      }
    }
  };
  return new OpenFgaClient(configuration);
});

//👆 new code

var app = builder.Build();

//...existing code...
```

The client is configured as a singleton using the settings coming from the `appsettings.json` file.

### Write Tuples on Resource Creation

When a user creates a folder, we need to tell Auth0 FGA that this user is the owner of the new folder. We'll do this by writing a "tuple." A tuple is a single piece of relationship data with the structure `(User, Relation, Object)`.

Let's modify the `MapPost()` handler on the  `/documents/folders` endpoint as follows:

```c#
documentsApi.MapPost("/folders",
    async (CreateFolderRequest req, HttpContext context, OpenFgaClient fgaClient) =>
    {
      //👇 changed code
      var userId = GetUserId(context);
      var newFolder = new Folder(Guid.NewGuid().ToString(), req.Name, userId);
      //👆 changed code
      folders[newFolder.Id] = newFolder;

      //👇 new code
      var tuple = new ClientTupleKey
      {
        User = $"user:{userId}",
        Relation = "owner",
        Object = $"folder:{newFolder.Id}"
      };

      await fgaClient.WriteTuples(new List<ClientTupleKey> { tuple });
      //👆 new code

      return Results.Created($"/documents/folders/{newFolder.Id}", newFolder);
    });
```

You've done a few things here:

* Injected the `OpenFgaClient` into your endpoint handler.  
* Assigned the user ID to the `userId` variable so that it’s accessible to the rest of the handler.  
* After creating the folder in your local database, you created a `ClientTupleKey` that represents the statement “the current user is the owner of the newly created folder”.  
* Then you sent this tuple to Auth0 FGA using the `fgaClient.WriteTuples()` method.

The same logic applies to creating a file.

### Check Permissions Before Modifying Resources

Now for the most important part: enforcing the authorization rules. Let's focus on the `MapPut()` handler of the endpoint for folders. Before we allow the update, we will ask Auth0 FGA if the current user `can_edit` the folder.

You could add the code directly to the endpoint handler, but that would require duplicating the code for each endpoint and complicating the handler code itself. Instead, you can leverage ASP.NET Core support for defining [authorization policies](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies). This allows for better separation of concerns and improves the readability of your endpoint's handler code.

Let’s start by defining an [authorization requirement](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement) class:

```c#
public class ItemPermissionRequirement : IAuthorizationRequirement
{
  public string Relation { get; }
  public string ResourceType { get; }
  public ItemPermissionRequirement(string relation, string resourceType)
  {
    Relation = relation;
    ResourceType = resourceType;
  }
}
```

This class just describes the permission that the current user must have. In our case, it defines the relationship (`Relation` property) between the current user and the resource type (`ResourceType` property) they want to access.

Then, you need to define an [authorization handler](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler), the component responsible for actually verifying the user’s permissions. Here is the code for it:

```c#
public class ItemPermissionHandler : AuthorizationHandler<ItemPermissionRequirement>
{
  private readonly OpenFgaClient _fgaClient;
  private readonly IHttpContextAccessor _httpContextAccessor;

  public ItemPermissionHandler(OpenFgaClient fgaClient, IHttpContextAccessor httpContextAccessor)
  {
    _fgaClient = fgaClient;
    _httpContextAccessor = httpContextAccessor;
  }

  protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ItemPermissionRequirement requirement)
  {
    var httpContext = _httpContextAccessor.HttpContext;
    var userId = httpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);

    var routeValues = httpContext?.Request.RouteValues;
    if (routeValues == null || !routeValues.TryGetValue("id", out var idObj))
    {
      context.Fail();
      return;
    }
    var id = idObj?.ToString();

    var checkRequest = new ClientCheckRequest
    {
      User = $"user:{userId}",
      Relation = requirement.Relation,
      Object = $"{requirement.ResourceType}:{id}"
    };

    var response = await _fgaClient.Check(checkRequest);

    if (response.Allowed == true)
      context.Succeed(requirement);
    else
      context.Fail();
  }
}
```

Here's a summary of what the authorization handler does:

* In the constructor, it takes the OpenFGA client instance and the current HTTP context.
* In the `HandleRequirementAsync()` method, it takes the authorization requirement as input.
* It then retrieves the current user ID from the HTTP context and the resource ID from the endpoint’s `id` parameter.
* It then composes an instance of the `ClientCheckRequest` class representing the tuple to be verified and sends it to Auth0 FGA for evaluation.
* The outcome of the verification on Auth0 FGA determines the authorization handler's result.

Now that you have the authorization requirement and handler classes, use them to enable the actual authorization check.

Go to your `Program.cs` file and replace the line `builder.Services.AddAuthorization();` with the following code:

```c#
//...existing code...

builder.Services.AddAuthentication().AddJwtBearer();

//👇 new code
builder.Services.AddHttpContextAccessor();

builder.Services.AddSingleton<IAuthorizationHandler, ItemPermissionHandler>();

builder.Services.AddAuthorization(options =>
{
  options.AddPolicy("CanEditFolder", policy =>
      policy.Requirements.Add(new ItemPermissionRequirement("can_edit", "folder")));
  options.AddPolicy("CanDeleteFolder", policy =>
      policy.Requirements.Add(new ItemPermissionRequirement("owner", "folder")));
  options.AddPolicy("CanEditFile", policy =>
      policy.Requirements.Add(new ItemPermissionRequirement("can_edit", "file")));
  options.AddPolicy("CanDeleteFile", policy =>
      policy.Requirements.Add(new ItemPermissionRequirement("owner", "file")));
});
//👆 new code

//...existing code...
```

The new code registers the service to allow the authorization handler to access the HTTP context, registers the authorization handler as a singleton, and configures the authorization service with four policies corresponding to the permissions to modify and delete files and folders.

Finally, apply the appropriate policies to the endpoints by using `RequireAuthorization()`, as in the following:

```c#
//...existing code...

documentsApi.MapPut("/folders/{id}",
    async (string id, UpdateFolderRequest req) =>
    {
      if (!folders.ContainsKey(id)) return Results.NotFound();

      var updatedFolder = folders[id] with { Name = req.Name };
      folders[id] = updatedFolder;
      return Results.Ok(updatedFolder);
    })
    .RequireAuthorization("CanEditFolder"); //👈 new code

documentsApi.MapDelete("/folders/{id}", (string id) =>
{
  return folders.Remove(id) ? Results.NoContent() : Results.NotFound();
})
.RequireAuthorization("CanDeleteFolder"); //👈 new code

//...existing code...
```

<include src="ebook-ads/dotnetIdentity" />

## Conclusion

You have successfully transformed your ASP.NET Core API from a coarse-grained authorization system to one with a fine-grained authorization. By integrating Auth0 FGA, you offloaded the complex authorization logic from your application code to a dedicated, powerful service.

This approach offers several key advantages:

* **Decoupling**: Your authorization logic lives separately from your application code, making it easier to manage and update.
* **Clarity**: The Auth0 FGA modeling language provides a clear, auditable definition of your authorization rules.
* **Flexibility**: You can easily change your model to support new features like sharing files, creating teams, or inheriting permissions without significant changes to your API code. You just update the model and the tuples you write.

You now have a solid foundation for building sophisticated, relationship-based authorization into any ASP.NET Core application. Check out the [Auth0 FGA documentation](https://docs.fga.dev/) and [give it a try](https://a0.to/auth0-fga-content) now!
