Skip to content
Alvo is pre-v0.1: the image runs from its edge tag, and no NuGet package or release is published yet.Pre-v0.1: no release yet.Roadmap and status

Use your own authentication

Let the users who sign in to your embedded app read and write Alvo's data under the descriptor's rules, without writing authorization code of your own.

  • An app that embeds Alvo as in Embed Alvo in ASP.NET Core. This page walks through one Program.cs; to try it, step 4 runs the repository’s embedded sample, which has the same endpoints, from a clone with the .NET 10 SDK, plus curl and jq.
  • Its descriptor, examples/vehicle-registry, the same one Run your own descriptor serves from Docker. It lets admin and inspector update a vehicle and every authenticated caller read one.

An embedded host serves the same data to two kinds of caller, and they authenticate differently:

PathWhoHow they reach the data
/app/*your app’s own users, signed in with your app’s cookieyour endpoints call IAlvoData with an AlvoContext they build from the user’s claims
/api/alvo/*agents and machines, with X-Alvo-Api-KeyAlvo’s generated Data API, mapped by MapAlvoDataApi()
/health/live, /health/readycontainer probesMapAlvoHealth()

Both go through the same rule engine. Your endpoints contain no authorization logic: they say who the caller is, and the descriptor’s rules decide.

Alvo adds no authentication middleware to your app. This one signs its users in with an ordinary ASP.NET Core cookie, and sets the cookie’s options explicitly: SameSite=Lax keeps a cross-site form from carrying it, Secure is required outside development, and a session ends after eight hours.

Program.cs
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "fleet-desk";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = builder.Environment.IsDevelopment()
? CookieSecurePolicy.SameAsRequest
: CookieSecurePolicy.Always;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = false;
});
builder.Services.AddAuthorization();

Its sign-in endpoint, /app/login, exists only in the Development environment and checks no password: it takes a demo user’s name and reads that user’s roles from a table on the server. It never takes a role list from the request, because a sign-in that lets callers name their own roles lets anyone become an administrator:

Program.cs
var demoUsers = new Dictionary<string, (Guid Id, string[] Roles)>(StringComparer.OrdinalIgnoreCase)
{
["inspector"] = (Guid.Parse("3f6b9c21-5a4d-4e88-9b2f-7c1a0d5e6f30"), ["inspector"]),
["clerk"] = (Guid.Parse("b8a45d17-2e93-4c60-8f1d-6a2b3c4d5e6f"), []),
};
app.MapPost("/app/login", (LoginRequest request) =>
{
if (!demoUsers.TryGetValue(request.User, out var user))
{
return Results.NotFound(new { known = demoUsers.Keys });
}
var identity = new ClaimsIdentity(
[new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), .. user.Roles.Select(role => new Claim(ClaimTypes.Role, role))],
CookieAuthenticationDefaults.AuthenticationScheme);
return Results.SignIn(new ClaimsPrincipal(identity), authenticationScheme: CookieAuthenticationDefaults.AuthenticationScheme);

Replace it with your own identity provider. What has to survive the replacement is that the roles come from somewhere the caller does not control.

3. Turn a signed-in user into an AlvoContext

Section titled “3. Turn a signed-in user into an AlvoContext”

Every /app endpoint starts by building the caller Alvo will judge:

Program.cs
if (roles.DeclaredRoles is not { } catalog)
{
return Results.Problem(detail: "Alvo has not applied a descriptor yet.", statusCode: StatusCodes.Status503ServiceUnavailable);
}
if (!Guid.TryParse(http.User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
{
return Results.Unauthorized();
}
var caller = new AlvoContext
{
User = new UserId(userId),
Roles = catalog.Resolve(["authenticated", .. http.User.FindAll(ClaimTypes.Role).Select(c => c.Value).Where(r => catalog.TryGet(r, out _))]),
};
  • Roles go through the role catalog. IRoleCatalogProvider.DeclaredRoles holds the three built-in roles plus the applied descriptor’s auth.roles. RoleCatalog.Resolve mints roles only from that set and throws UnknownRoleException for any other name, so a typo is refused where it arrives instead of silently matching no rule. The code filters your app’s roles with TryGet first: a role your app knows and the descriptor does not is dropped, because only the overlap means anything to Alvo’s rules.
  • authenticated is added for every signed-in user, because the descriptor’s read rules key on it.
  • No descriptor yet is a 503, not a 401. DeclaredRoles is null until Alvo has applied the descriptor at start, which is a boot problem rather than a sign-in problem.
  • No tenant here, because this descriptor declares no tenancy. Over a tenant-scoped entity, set AlvoContext.Tenant from your user’s tenant, or every request is refused before any rule runs (Multi-tenancy).

Call Alvo from your endpoints shows the endpoints that use it.

Start the sample in the background over a fresh database. The dev key’s secret comes from an environment variable, because the sample ships no credential:

Terminal (in the clone)
export FLEET_DESK_KEY_SECRET="$(openssl rand -hex 16)"
Alvo__Auth__DevKeys__0__Secret="$FLEET_DESK_KEY_SECRET" \
dotnet run --project samples/MMLib.Alvo.Samples.EmbeddedHost -- \
--FleetDesk:DatabasePath "$(mktemp -d)/fleet-desk.db" &
until curl -sf localhost:5199/health/ready > /dev/null; do sleep 1; done

Create a vehicle with the API key, sign in as the two demo users, and let each repaint it:

Terminal (in the clone)
KEY="agent.$FLEET_DESK_KEY_SECRET"
OWNER=$(curl -s -X POST localhost:5199/api/alvo/owners -H "X-Alvo-Api-Key: $KEY" \
-H "Content-Type: application/json" -d '{"name":"Fleet Desk Ltd"}' | jq -r .id)
VEHICLE=$(curl -s -X POST localhost:5199/api/alvo/vehicles -H "X-Alvo-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"vin":"TMBJJ7NE8L0123456","plate":"BA-101AA","make":"Skoda","model":"Octavia","year":2020,"owner_id":"'"$OWNER"'"}' \
| jq -r .id)
curl -s -c inspector.cookies -X POST localhost:5199/app/login \
-H "Content-Type: application/json" -d '{"user":"inspector"}'
curl -s -c clerk.cookies -X POST localhost:5199/app/login \
-H "Content-Type: application/json" -d '{"user":"clerk"}'
curl -s -b inspector.cookies -X PATCH "localhost:5199/app/vehicles/$VEHICLE" \
-H "Content-Type: application/json" -d '{"color":"red"}' -w ' %{http_code}\n'
curl -s -b clerk.cookies -X PATCH "localhost:5199/app/vehicles/$VEHICLE" \
-H "Content-Type: application/json" -d '{"color":"blue"}' -w ' %{http_code}\n'

The inspector’s PATCH answers 200 with the repainted vehicle, and its updated_by is the inspector’s user id, the Id from the table in step 2. The clerk’s answers 404 with no body: the update rule is a row filter, so for a caller it excludes the row is not there, the same answer a key would get from the generated API. The sample contains no code for either decision. A request with no cookie at all never reaches Alvo; the app’s own authorization answers it, with a redirect to a sign-in page. Stop the sample with kill %1.

IAlvoData takes the caller as an explicit AlvoContext parameter on every call, so your endpoint states who it acts as and Alvo enforces the rules inside the call, over the same compiled policy the generated API uses. The generated routes resolve their own caller from the API-key header and ignore whatever your middleware does. Standalone and embedded compares the two modes.

Section titled “Never read Alvo’s credential from a cookie”

Alvo:Auth:HeaderName names the header the generated routes read their API key from, and it is plain configuration. Pointing it at Cookie, together with a custom IAlvoContextResolver, would make the browser authenticate Alvo’s routes for you, and would make every one of them a cross-site request forgery target: a browser attaches cookies to cross-site requests by itself. So Alvo refuses that configuration at startup:

Terminal (in the clone)
Alvo__Auth__DevKeys__0__Secret="$FLEET_DESK_KEY_SECRET" Alvo__Auth__HeaderName=Cookie \
dotnet run --project samples/MMLib.Alvo.Samples.EmbeddedHost

The host stops before it listens, with an OptionsValidationException whose message says that Alvo:Auth:HeaderName is Cookie, why that is refused, that the default is X-Alvo-Api-Key, and to resolve your users in your own endpoints instead. Alvo:Auth:TenantHeaderName is refused the same way. Authorization is allowed: a browser does not attach it to a cross-site request unless the user has signed in to that site with HTTP authentication, and script cannot set it cross-origin without the server’s consent.

The JSON Content-Type requirement on every request with a body is the other half of that defence, and it has no switch (Write data safely). Your own endpoints are yours to protect: a cookie-authenticated /app route that changes data needs the same care as any other in your app.

StatusProblem typeWhenFixReturned by
401unauthenticatedAn API key sent to /api/alvo cannot be used.Use the configured secret; give the key only roles the descriptor declares.every host
403none, your app’s ownA rule or a before-hook refused (AlvoAuthorizationException); the code above renders it as a problem document with the exception’s message.Check the rule against the user’s roles.your endpoints
404none, your app’s ownA rule excludes the row for this user (AlvoRecordNotFoundException).Check the rule and the user’s roles.your endpoints
503none, your app’s ownDeclaredRoles is null: no descriptor has been applied yet.Wait for /health/ready; read the log if it never turns ready.your endpoints
nonenoneThe host stops at start: Alvo:Auth:HeaderName or TenantHeaderName is Cookie, or a dev key’s secret is missing or shorter than 32 characters.Use a header only script can set; supply a secret of at least 32 characters (openssl rand -hex 16).startup

Call Alvo from your endpoints: the /app endpoints themselves, and how to mount the generated API beside them.