-
Notifications
You must be signed in to change notification settings - Fork 0
03 First Protected API
This chapter builds the smallest complete ASP.NET Core flow:
- authenticate a caller;
- compile
rulegate.yaml; - register RuleGate;
- map one endpoint to
document/read; - observe
401,403, and200outcomes.
The sample authentication handler uses headers only to keep the chapter focused. Never use caller-controlled identity headers in production.
dotnet new web -n RuleGateQuickstart
cd RuleGateQuickstart
dotnet add package Fotbiler.RuleGate.AspNetCore --version 1.0.0
dotnet tool install Fotbiler.RuleGate.Cli --version 1.0.0 \
--create-manifest-if-neededCreate rulegate.yaml:
schemaVersion: 1
application:
id: rulegate-quickstart
name: RuleGate Quickstart
policies:
- id: document-read
resourceType: document
action: read
requirement:
all:
- any:
- permission: DOC.READ
- role: DOCUMENT.READER
- not:
role: DOCUMENT.BLOCKEDRead it as:
The caller may read a document when the caller has either
DOC.READor theDOCUMENT.READERrole, and does not haveDOCUMENT.BLOCKED.
Validate before starting the host:
dotnet tool run rulegate validate rulegate.yamlFix validation errors rather than starting with a partial policy set.
Add this to the project file:
<ItemGroup>
<Content Include="rulegate.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>Replace Program.cs with the following. The authentication handler appears
after the application code.
using System.Security.Claims;
using System.Text.Encodings.Web;
using Fotbiler.RuleGate.AspNetCore.DependencyInjection;
using Fotbiler.RuleGate.AspNetCore.Endpoints;
using Fotbiler.RuleGate.Manifest.Compilation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
var manifestPath = Path.Combine(
builder.Environment.ContentRootPath,
"rulegate.yaml");
var compilation = await new RuleGateManifestCompiler()
.CompileFromFileAsync(manifestPath);
if (!compilation.IsSuccess)
{
throw new InvalidOperationException(
"rulegate.yaml is invalid. Run 'rulegate validate' for details.");
}
builder.Services
.AddAuthentication("Demo")
.AddScheme<AuthenticationSchemeOptions, DemoAuthenticationHandler>(
"Demo",
configureOptions: null);
builder.Services.AddAuthorization();
builder.Services
.AddRuleGate()
.AddHttpAuthorizationResultMapping()
.AddPolicies(compilation.Policies);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet(
"/documents/{id}",
(string id) => Results.Ok(new
{
id,
title = "Reference document",
}))
.RequireRuleGate("document", "read", "id");
app.Run();Registration order matters:
- authentication establishes
HttpContext.User; - ASP.NET Core authorization hosts RuleGate's handler;
-
AddRuleGate()registers the engine and built-in evaluators; - policies are immutable definitions used by the engine;
- authentication middleware runs before authorization middleware.
Append this class to Program.cs:
public sealed class DemoAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: AuthenticationHandler<AuthenticationSchemeOptions>(
options,
logger,
encoder)
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("X-Demo-User", out var userId))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId.ToString()),
};
foreach (var permission in Request.Headers["X-Demo-Permissions"]
.ToString()
.Split(',', StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries))
{
claims.Add(new Claim("permission", permission));
}
foreach (var role in Request.Headers["X-Demo-Roles"]
.ToString()
.Split(',', StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries))
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}Production authentication must validate issuer, audience, signature, expiration, and the application-specific token requirements. RuleGate starts after that validation.
dotnet runUse the URL printed by ASP.NET Core.
Anonymous request:
curl -i http://localhost:5000/documents/doc-1Expected: 401. Authentication did not establish a subject.
Authenticated without a grant:
curl -i http://localhost:5000/documents/doc-1 \
-H 'X-Demo-User: alice'Expected: 403. The subject exists but the policy is not satisfied.
Permission path:
curl -i http://localhost:5000/documents/doc-1 \
-H 'X-Demo-User: alice' \
-H 'X-Demo-Permissions: DOC.READ'Expected: 200.
Role path:
curl -i http://localhost:5000/documents/doc-1 \
-H 'X-Demo-User: alice' \
-H 'X-Demo-Roles: DOCUMENT.READER'Expected: 200.
Explicit block:
curl -i http://localhost:5000/documents/doc-1 \
-H 'X-Demo-User: alice' \
-H 'X-Demo-Permissions: DOC.READ' \
-H 'X-Demo-Roles: DOCUMENT.BLOCKED'Expected: 403 because the not requirement is not satisfied.
For /documents/doc-1, the integration constructs approximately:
subject.id = alice
subject.roles = values mapped from ClaimTypes.Role
subject.permissions = values mapped from permission claims
resource.type = document
resource.id = doc-1
action = read
context.evaluationTime = trusted server clock
The default resource mapper does not query a database. Later chapters add a resource provider that loads owner, organization, status, and classification.
The repository's Minimal ASP.NET Core sample contains a broad manifest catalog and deterministic policy fixtures. The document-approval application shows real Keycloak authentication, SQLite-backed resource data, Angular, and the complete authorization workflow.
- 401 instead of 403: authentication did not create an authenticated principal, or authentication middleware is missing.
- 403 for every user: the claim type or exact permission/role casing does not match.
- Manifest not found: copy it to output/publish or use an absolute path built from the content root.
-
Policy not found:
resourceTypeandactiondo not exactly match the endpoint metadata. - Unexpected allow assumption: a valid token authenticates; it does not automatically grant a RuleGate policy.
Previous: Packages and installation · Next: Policy language
Canonical source: docs/guide · Documentation index · RuleGate 1.0.0
- Home
- 1. Authorization foundations
- 2. Packages and installation
- 3. First protected API
- 4. Policy language
- 5. ASP.NET Core integration
- 6. Trusted attributes and context
- 7. Identity and Keycloak
- 8. Frontend integration
- 9. CLI and policy lifecycle
- 10. Testing and diagnostics
- 11. Policy sources and reload
- 12. Extensibility
- 13. Real-world recipes
- 14. Production checklist
- Glossary