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.
Before you start
Section titled “Before you start”- 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, pluscurlandjq. - Its descriptor,
examples/vehicle-registry, the same one Run your own descriptor serves from Docker. It letsadminandinspectorupdate a vehicle and every authenticated caller read one.
1. Two surfaces, one backend
Section titled “1. Two surfaces, one backend”An embedded host serves the same data to two kinds of caller, and they authenticate differently:
| Path | Who | How they reach the data |
|---|---|---|
/app/* | your app’s own users, signed in with your app’s cookie | your endpoints call IAlvoData with an AlvoContext they build from the user’s claims |
/api/alvo/* | agents and machines, with X-Alvo-Api-Key | Alvo’s generated Data API, mapped by MapAlvoDataApi() |
/health/live, /health/ready | container probes | MapAlvoHealth() |
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.
2. Sign your users in, your way
Section titled “2. Sign your users in, your way”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.
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:
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:
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.DeclaredRolesholds the three built-in roles plus the applied descriptor’sauth.roles.RoleCatalog.Resolvemints roles only from that set and throwsUnknownRoleExceptionfor any other name, so a typo is refused where it arrives instead of silently matching no rule. The code filters your app’s roles withTryGetfirst: a role your app knows and the descriptor does not is dropped, because only the overlap means anything to Alvo’s rules. authenticatedis added for every signed-in user, because the descriptor’s read rules key on it.- No descriptor yet is a 503, not a 401.
DeclaredRolesisnulluntil 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.Tenantfrom 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.
4. Try it
Section titled “4. Try 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:
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; doneCreate a vehicle with the API key, sign in as the two demo users, and let each repaint it:
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.
How it works
Section titled “How it works”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.
Never read Alvo’s credential from a cookie
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:
Alvo__Auth__DevKeys__0__Secret="$FLEET_DESK_KEY_SECRET" Alvo__Auth__HeaderName=Cookie \ dotnet run --project samples/MMLib.Alvo.Samples.EmbeddedHostThe 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.
What can go wrong
Section titled “What can go wrong”| Status | Problem type | When | Fix | Returned by |
|---|---|---|---|---|
| 401 | unauthenticated | An API key sent to /api/alvo cannot be used. | Use the configured secret; give the key only roles the descriptor declares. | every host |
| 403 | none, your app’s own | A 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 |
| 404 | none, your app’s own | A rule excludes the row for this user (AlvoRecordNotFoundException). | Check the rule and the user’s roles. | your endpoints |
| 503 | none, your app’s own | DeclaredRoles is null: no descriptor has been applied yet. | Wait for /health/ready; read the log if it never turns ready. | your endpoints |
| none | none | The 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 |
Reference
Section titled “Reference”- C# API:
IAlvoData,IAlvoContextAccessor,IAlvoContextResolver. - Configuration:
Alvo:Auth. - The runnable sample step 4 starts:
samples/MMLib.Alvo.Samples.EmbeddedHost. - Design notes: the identity seam in
extensibility.mdand why a media type is a CSRF control.
Call Alvo from your endpoints: the /app endpoints themselves, and how
to mount the generated API beside them.