Externalize authorization in a .NET (C#) app so policy changes don't need a redeploy
Short answer: To externalize authorization in a .NET app, call permit.Check() from your request handlers and keep the rules in Permit, so a policy change needs no redeploy. Use it when roles and permissions change more often than your releases. Don't use it when a few fixed role checks in one application cover your needs.
| Fact | Value |
|---|---|
| Question this page answers | How do I externalize authorization in .NET so policy changes don't need a redeploy? |
| Where the check runs | HandlePosts, which calls await permit.Check(). |
| SDK | Permit on NuGet. The SDK feature parity page lists version 1.8.0. See Check permissions with the .NET SDK. The tutorial server uses HttpListener, not the ASP.NET Core pipeline. |
| PDP | A container PDP at http://localhost:7766 in this tutorial, or the Cloud PDP. See PDP overview. |
| Models used | RBAC for the Author and Reader roles. ReBAC and ABAC are also supported. |
| Policy changes | You change roles and permissions in the Permit dashboard. The application code and the deployment stay the same. |
| Free tier | Community plan, labeled "Free Forever" on the Permit.io pricing page: MAU 1000, Tenants 20, Authorization Queries No Limit, Environments 3, PDP Instances No Limit. |
| Audit | Each check appears on the Audit Log screen of the Permit dashboard. |
| Open source | The Edge PDP (permitio/PDP), OPAL, cedar-agent, the Permit SDKs, and the Permit CLI are open source. See Open-source fallback. |
Build a C# HTTP server for a blogging platform that registers users in Permit.io and allows only users with the Author role to create posts. This tutorial is for .NET backend developers who want to enforce Permit.io policies from their request handlers. The example uses HttpListener from the .NET class library, so it runs as a console project without the ASP.NET Core pipeline.
When you finish, your app has two endpoints:
| Endpoint | What it does |
|---|---|
POST /register | Syncs a user to Permit.io and assigns the user the Reader role in the default tenant |
POST /posts | Calls permit.Check() and returns 403 unless the user in the request body has permission to create a Post |
Prerequisites
- A Permit.io account. See Create a Permit.io account.
- The .NET SDK and a console project, for example one created with
dotnet new console. - Node.js and npm, to install the Permit CLI.
- Docker, to run the policy decision point (PDP) container.
1. Configure the policy in Permit
Create the blogging platform policy with the Permit CLI. If your environment already has a policy with a Post resource and an Author role that can create posts, skip to 2. Get your API key.
Install the Permit CLI
The Permit CLI creates policies and runs the PDP from your terminal. Install the CLI with npm:
npm install -g @permitio/cli
Run permit to confirm that the CLI is installed.
Sign in with the Permit CLI
Authenticate the CLI with your Permit.io account:
permit login
The command opens a browser window where you sign in. After you sign in, the CLI uses your default environment. To use a different environment, run permit env select and choose the environment.
Apply the blogging platform template
Permit CLI templates create a policy with predefined resources, roles, and rules. To see the available templates, run permit env template list. The template source files are in the Permit CLI repository.

Apply the blogging-platform template to your environment:
permit env template apply --template blogging-platform
The CLI prints a success message when the template is applied.
Review the policy in the Policy Editor
In the Permit dashboard, select your project and open the Policy screen.

The blogging-platform template creates:
| Policy element | What the template defines |
|---|---|
| Resources | Post (with a premium boolean attribute) and Comment, each with create, read, update, and delete actions |
| Roles | Admin (all actions), Author (create and read posts, read comments), Reader (create and read comments), and Premium Reader (read posts and comments) |
| Relationship | A Post is the parent of its Comment instances. An Author of a post instance becomes a Moderator of the comments on that post. This rule is relationship-based access control (ReBAC). |
| Resource set | Free Post contains posts where premium is false. Readers can read free posts. This rule is attribute-based access control (ABAC). |
This tutorial uses one rule from the policy: the Author role can create a Post, and the Reader role cannot. To change which role can perform an action, check or clear the box in the Policy Editor.
2. Get your API key
Your .NET app and the PDP authenticate with Permit.io with your environment API key. Copy the API key of the environment where you applied the template. See Get your API key.
Anyone with the environment API key can change that environment's policy through the Permit API. Load the key from an environment variable, and don't commit it.
3. Run the PDP
The PDP evaluates each permission check against your policy. Start a PDP container with the Permit CLI:
permit pdp run
The command starts the PDP in Docker and prints the container ID and name. The PDP listens on port 7766, so your app connects to it at http://localhost:7766.

The Free Post resource set is an ABAC rule, and the Cloud PDP doesn't evaluate ABAC rules, so run the container PDP for this policy. To run the container with docker run instead, or to check that the PDP is healthy, see Run the PDP.
4. Build the .NET app
All the C# code in this section goes in Program.cs, inside the HttpServer class.
Install the .NET SDK
In your project directory, add the Permit .NET SDK package:
dotnet add package Permit
For all SDK options, see Check permissions with the .NET SDK.
Create the server and the Permit client
Replace the contents of Program.cs with the following code:
// Program.cs
using System;
using System.Text;
using System.Net;
using System.Threading.Tasks;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using PermitSDK;
using PermitSDK.Models;
using PermitSDK.OpenAPI.Models;
namespace PermitOnboardingApp
{
// HttpServer class to handle the incoming connections and route the requests to the appropriate endpoints
class HttpServer
{
public static HttpListener listener;
public static string url = "http://localhost:8000/";
public static string clientToken = Environment.GetEnvironmentVariable("PERMIT_API_KEY");
public static string pdpUrl = Environment.GetEnvironmentVariable("PDP_URL") ?? "http://localhost:7766";
public static Permit permit = new Permit(
clientToken,
pdpUrl,
"default",
true
);
// HandleIncomingConnections function to handle the incoming connections and route the requests to the appropriate endpoints
public static async Task HandleIncomingConnections()
{
bool runServer = true;
while (runServer)
{
HttpListenerContext ctx = await listener.GetContextAsync();
HttpListenerRequest req = ctx.Request;
HttpListenerResponse resp = ctx.Response;
string path = req.Url.AbsolutePath.ToLower();
string method = req.HttpMethod.ToUpper();
// Handle the incoming connections and route the requests to the appropriate endpoints
try
{
if (method == "POST" && path == "/register")
{
await HandleRegister(req, resp);
}
else if (method == "POST" && path == "/posts")
{
await HandlePosts(req, resp);
}
else
{
resp.StatusCode = 404;
await SendJsonAsync(resp, new { error = "Not found" });
}
}
catch (Exception ex)
{
resp.StatusCode = 500;
await SendJsonAsync(resp, new { error = ex.Message });
}
}
}
}
}
The HttpServer class creates a Permit client and routes requests:
| Request | Handler |
|---|---|
POST /register | HandleRegister |
POST /posts | HandlePosts |
| Any other request | Returns 404 with "Not found" |
The Permit client reads the API key from the PERMIT_API_KEY environment variable. Set that variable to your environment API key from 2. Get your API key. The client connects to the PDP at the address in the PDP_URL environment variable, and uses http://localhost:7766, the address from 3. Run the PDP, when PDP_URL isn't set.
Add the Main method and the JSON helper
Add the following methods inside the HttpServer class:
// Program.cs
// SendJsonAsync function to send a JSON response
public static async Task SendJsonAsync(HttpListenerResponse resp, object obj)
{
resp.ContentType = "application/json";
var json = JsonSerializer.Serialize(obj);
byte[] data = Encoding.UTF8.GetBytes(json);
await resp.OutputStream.WriteAsync(data, 0, data.Length);
resp.Close();
}
public static void Main(string[] args)
{
listener = new HttpListener();
listener.Prefixes.Add(url);
listener.Start();
Console.WriteLine("Listening for connections on {0}", url);
Task listenTask = HandleIncomingConnections();
listenTask.GetAwaiter().GetResult();
listener.Close();
}
Main starts an HttpListener on http://localhost:8000/ and waits for requests. SendJsonAsync writes an object to the response as JSON and closes the response.
Add the /register handler
Add the following code inside the HttpServer class. HandleRegister syncs the user to Permit.io with permit.Api.SyncUser(), then assigns the user the Reader role in the default tenant with permit.Api.AssignRole().
// Program.cs
public class RegisterRequest
{
public string email { get; set; }
public string first_name { get; set; }
public string last_name { get; set; }
}
// HandleRegister function to register a user
public static async Task HandleRegister(HttpListenerRequest req, HttpListenerResponse resp)
{
using var reader = new StreamReader(req.InputStream, req.ContentEncoding);
var body = await reader.ReadToEndAsync();
var data = JsonSerializer.Deserialize<RegisterRequest>(body);
if (data == null || string.IsNullOrEmpty(data.email) || string.IsNullOrEmpty(data.first_name) || string.IsNullOrEmpty(data.last_name))
{
resp.StatusCode = 400;
await SendJsonAsync(resp, new { error = "Missing required fields" });
return;
}
var userObj = new UserCreate {
Key = data.email,
Email = data.email,
First_name = data.first_name,
Last_name = data.last_name,
Attributes = new System.Collections.Generic.Dictionary<string, object>()
};
var user = await permit.Api.SyncUser(userObj);
var assignedRole = new {
user = data.email,
role = "Reader",
tenant = "default"
};
var roleAssignment = await permit.Api.AssignRole(assignedRole.user, assignedRole.role, assignedRole.tenant);
resp.StatusCode = 201;
await SendJsonAsync(resp, new { message = "User registered and role assigned", user, role_assignment = roleAssignment });
}
The handler returns 400 when email, first_name, or last_name is missing. The user's email address is the user key in Permit.io. HandlePosts passes the same key to permit.Check().
Add the /posts handler
Add the following code inside the HttpServer class. HandlePosts asks the PDP whether the user key in the user field of the request body can create a Post:
// Program.cs
// PostsRequest class to store the request body
public class PostsRequest
{
public string user { get; set; }
}
// HandlePosts function to check if the user has access to the resource
public static async Task HandlePosts(HttpListenerRequest req, HttpListenerResponse resp)
{
using var reader = new StreamReader(req.InputStream, req.ContentEncoding);
var body = await reader.ReadToEndAsync();
var data = JsonSerializer.Deserialize<PostsRequest>(body);
var action = "create";
var resource = "Post";
if (data == null || string.IsNullOrEmpty(data.user))
{
resp.StatusCode = 400;
await SendJsonAsync(resp, new { error = "Missing required fields" });
return;
}
bool permitted = await permit.Check(data.user, action, resource);
resp.StatusCode = permitted ? 200 : 403;
await SendJsonAsync(resp, new { message = permitted ? "User is permitted" : "User is not permitted" });
}
The handler returns "User is permitted", or 403 with "User is not permitted". The handler returns 400 when the user field is missing. To protect other endpoints, such as commenting or editing, change the action and resource values.
In a production app, take the user key from your authenticated session. This example reads the user key from the request body so that you can test the endpoint with curl.
Start the .NET app
Set the environment variables and start the app, replacing <YOUR_API_KEY> with your API key:
export PERMIT_API_KEY=<YOUR_API_KEY>
export PDP_URL=http://localhost:7766
dotnet run
The app prints Listening for connections on http://localhost:8000/.
5. Test the permission check
Register two users, give one of them the Author role, and confirm that the PDP allows only that user to create a post.
Register two users
In a second terminal, register John and Emma:
curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "first_name": "John", "last_name": "Doe"}'
curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "emma@example.com", "first_name": "Emma", "last_name": "Den"}'
Each request returns HTTP 201 with "message": "User registered and role assigned", the synced user under user, and the Reader role assignment under role_assignment.
Assign John the Author role
Both users have the Reader role, which can't create posts. Give John the Author role in the Permit dashboard:
- Open the Directory screen and select
john@example.comto open the Edit User panel. - Under Permissions Per Tenant, select the Default Tenant.
- In Top Level Access, add the Author role.
- Click Save.

For other ways to assign roles, including the API and SDK, see Sync users.
Check that John can create a post and Emma can't
Send a POST /posts request for John:
curl -X POST http://localhost:8000/posts \
-H "Content-Type: application/json" \
-d '{"user": "john@example.com"}'
The PDP allows the request because John has the Author role. The app returns {"message":"User is permitted"}.
Send the same request for Emma:
curl -X POST http://localhost:8000/posts \
-H "Content-Type: application/json" \
-d '{"user": "emma@example.com"}'
The PDP denies the request because Emma has only the Reader role. The app returns HTTP 403 with {"message":"User is not permitted"}.
Each check also appears in the Audit Log screen of the Permit dashboard, with the user, action, resource, and decision.
6. Change the policy without redeploying
Your .NET code contains the enforcement point: HandlePosts, which calls await permit.Check(). The rule about which role may create a post is policy in Permit, not code in your app. Change the rule and watch the app respond without restarting the .NET app.
- Keep the app and the PDP running, with John's Author role from 5. Test the permission check.
- In the Permit dashboard, open the Policy screen. Clear the box for the create action on Post in the Author role column.
- Send John's
POST /postsrequest from step 5 again. The PDP denies it, and the app returns the same403response that Emma received. - Check the box again and send the request once more. The app returns the successful response again.
Updates reach a PDP in the background, so repeat the request if you still see the previous result. The change doesn't appear in a pull request or a build, because it is a policy change and not a code change. To review policy changes like code, see GitOps.
Frequently asked questions
How do I externalize authorization in .NET so policy changes don't need a redeploy?
Keep the enforcement point in your code and move the rules to Permit. Your handler asks the PDP whether a user can perform an action on a resource, using the .NET SDK, and the PDP answers from the policy you edit in the Permit dashboard. Section 6 changes a rule and sends the same request again, with no code change.
Does a role or permission change require restarting the .NET app?
No. Roles, permissions, and role assignments live in Permit. The PDP receives updates in the background, so the next check uses the updated policy.
Where does the authorization decision run?
In the PDP, a separate service that your app calls over the network. This tutorial runs the PDP as a container at http://localhost:7766. The Cloud PDP is a managed alternative that supports RBAC and ReBAC. See Cloud PDP capabilities.
When should I keep authorization in .NET code instead?
When a small set of fixed roles lives in one application and changes only with a code release, checks in the application are enough. Use Permit.io when roles change more often than releases, several services share the rules, or non-developers manage access.
Which Permit SDK does this tutorial use?
Permit on NuGet. The SDK feature parity page lists version 1.8.0. See Check permissions with the .NET SDK. The tutorial server uses HttpListener, not the ASP.NET Core pipeline.
What does the free tier include?
The Community plan on the Permit.io pricing page lists MAU 1000, Tenants 20, Authorization Queries No Limit, and PDP Instances No Limit.
Next steps
- Check permissions with the .NET SDK: SDK installation, configuration, and more permission check examples.
- Check permissions with permit.check(): check against tenants, resource instances, and attributes.
- Build RBAC policies: create roles and permissions for your own resources.
- Deploy the PDP to production: run the PDP next to your services.