Apricot Framework

ASP.NET Core

Registration, attributes across all three transports, subject resolution and caching.

ApricotFramework.AccessAuthorization.AspNetCore supplies registration, the attributes, and the bridge from a ClaimsPrincipal to a subject.

builder.Services.AddAuthorization();
builder.Services.AddAccessAuthorization(builder.Configuration);
builder.Services.AddAccessStore<MyAccessStore>();

AddAccessAuthorization has the usual overloads: IConfiguration, IConfiguration plus a section name, an Action<AccessAuthorizationOptions>, or none for defaults.

One mechanism, three transports

The attributes implement IAuthorizationRequirementData and derive from AuthorizeAttribute, so the requirement travels as endpoint metadata and the authorization middleware composes it. Nothing is an MVC filter, which is why the same attribute governs all three:

[AuthorizeAnyAccess(ContentAccesses.OrdersRead)]                 // controller action
public object Get(string id) =>

[AuthorizeAllScopes("sales.read")]                             // gRPC method
public override Task<GetOrderReply> GetOrder(GetOrderRequest request, ServerCallContext context) =>

app.MapGet("/orders/{id}", GetOrder).RequireAccessAny(ContentAccesses.OrdersRead);   // minimal API

[AllowAnonymous] is honoured by the middleware, so this library carries no equivalent check of its own.

Note

Attributes are resource-free. They ask whether the subject holds the access at all, and the handler never inspects the route or loads anything, so context.Resource is always null for a rule reached this way. Gating on a particular object is an imperative check in the handler, where the object has been loaded — see usage.

OutcomeResponse
No credentials at all401, so a client knows to authenticate
Authenticated, access not granted403
Authenticated with no subject id, access required403

Note

A gRPC client sees 401 as Unauthenticated and 403 as PermissionDenied, but the failure is produced before the gRPC endpoint runs, so the response carries no gRPC trailers and the client appends its own "missing HTTP content-type" note to the message. The status code is correct; the extra text is standard behaviour for authorization failures on a gRPC endpoint, not specific to this library.

Resolving the subject

IAccessSubjectResolver turns the principal into an AccessSubject. The default reads sub, falling back to ClaimTypes.NameIdentifier — both are needed, because JWT bearer authentication rewrites sub to the latter unless inbound claim mapping is switched off.

Configured claims are lifted onto the subject as attributes, which is how an identity becomes composite without the library knowing what the qualifier means:

{
  "Authorization": {
    "Subject": { "AttributeClaims": { "org_id": "organization" } }
  }
}

A rule, or a store, then reads subject.Attributes["organization"]. Two subjects with the same id in different organisations are different subjects, including for caching.

A principal carrying no subject id resolves to nothing and every access is denied. That is the normal state of a machine token, which scopes covers instead.

Caching

Set CacheLifetime and a subject's assigned accesses are reused for that long, keyed by the whole subject. Nothing is cached by default. The entries are namespaced, so they cannot collide with anything else sharing the host's memory cache.

Reading capabilities from a request

var allowed = await httpContext.GetAllowedAccessesAsync(resource, cancellationToken);
var subject = httpContext.GetAccessSubject();

The first requires a catalog and throws without one; see usage.

On this page