Start here
Getting started
Wire Sentinel into an ASP.NET Core app — EF Core stores, the engine, the authentication handler, the endpoint groups — and make your first login, refresh and authenticated call.
This walkthrough takes an empty ASP.NET Core app to a working identity provider: password login, token refresh with rotation, and an authenticated profile call. It mirrors the wiring Sentinel’s own HTTP test host uses, so every line here is exercised by the test suite.
Install
dotnet add package Nuvora.Nexus.Sentinel
dotnet add package Nuvora.Nexus.Sentinel.AspNetCore
dotnet add package Nuvora.Nexus.Sentinel.Stores.EfCore
The meta package brings Sentinel.Core (the framework-free domain engine) with it.
Register the services
Ordering matters once, and only once: stores before the engine. Every registration is
TryAdd-idempotent, so the first one wins — registering the EF stores first lets them
claim the ports (ISigningKeyStore, IAuditStore, IDefinitionCatalogStore, …) that
AddSentinel() would otherwise default to in-memory implementations.
var builder = WebApplication.CreateBuilder(args);
// 1. Persistence: EF Core stores (PostgreSQL, SQL Server or SQLite).
builder.Services.AddSentinelEfCoreStores(o => o.UseNpgsql(connectionString));
// 2. The engine: login, tokens, snapshots, risk, abuse, audit.
builder.Services.AddSentinel();
// 3. HTTP: the authentication handler and endpoint plumbing.
builder.Services.AddSentinelAuthentication(o =>
{
o.Issuer = "https://id.example.com";
o.Audience = "example-api";
o.DefaultRealmId = realmId;
o.Transport = SentinelTokenTransport.BearerAndCookie;
});
var app = builder.Build();
SentinelAspNetOptions refuses an empty Audience — audience enforcement is not
optional in Sentinel; token type (typ) and audience are always validated.
What AddSentinel() deliberately does not default
Five ports have no default because a wrong guess would be a silent security decision:
IUserStore, IMfaStore, ISessionStore, ISubjectDataSource and
IMachineIdentityStore. AddSentinelEfCoreStores(...) provides all of them; if you skip
it, you must register your own.
The database
The EF adapter exposes one model in two ways: use SentinelDbContext directly, or call
modelBuilder.ApplySentinelModel() inside your own DbContext to host Sentinel’s tables
next to yours. No migrations ship yet (the schema is still settling in Wave 1) — create
the schema with Database.EnsureCreated() in dev/tests or host-managed tooling; EF
migrations land before 1.0.
Mount the endpoints
app.UseAuthentication();
app.MapSentinelAuth(); // POST /auth/login, /auth/mfa/verify, /auth/refresh,
// /auth/logout, /auth/logout-all
app.MapSentinelProfile(); // GET /profile/me, /profile/sessions, /profile/permissions
Both accept a prefix argument (MapSentinelAuth("/identity")) and respect the host’s
PathBase. The rest of the surface mounts the same way when you need it:
MapSentinelPasskeys(), MapSentinelFederation(), MapSentinelOidc(),
MapSentinelSaml(), MapSentinelScim(), MapSentinelAdmin().
Keys: fail fast, on purpose
Sentinel will not mint tokens with keys it invented silently. In production the signing key ring loads from the persisted store (the EF adapter stores PKCS#8 keys and handles rotation with a 7-day overlap window by default). For development you opt in to ephemeral keys explicitly:
// Development only — keys are regenerated on every start, loudly.
builder.Services.AddSingleton(new SentinelKeyOptions
{
AllowEphemeralDevelopmentKeys = true,
});
Initialize the ring during startup with SentinelHost.InitializeAsync(app.Services) —
it fails fast with an explanation rather than serving a broken JWKS.
First login
Seed a user through the EF stores (or the admin API), then:
POST /auth/login
Content-Type: application/json
{ "email": "ada@example.com", "password": "hunter2!" }
Three outcomes, all deliberate:
- 200 with tokens —
{ "status": "ok", "accessToken": "…", "refreshToken": "srt_…" }. With the cookie transport, tokens travel as httpOnly cookies instead and the JSON body omits them; a non-httpOnlysentinel_csrfcookie pairs with theX-Sentinel-Csrfheader for double-submit CSRF protection on unsafe methods. - 200 with
"status": "mfa_required"— the body carries a single-purposemfaPendingToken(a signed JWT withtyp: "mfa+sentinel", five-minute lifetime) and the factor to satisfy ("totp"or"email_otp"). Complete withPOST /auth/mfa/verify. - Problem+json with a stable
errorcode —invalid_credentials(401),blocked(429), orcaptcha_required(429, with the site key in the extensions). Messages are localized fromAccept-Language; codes never change.
A failed lookup burns a dummy argon2id hash so that “no such user” and “wrong password” are indistinguishable by timing — see the user enumeration recipe.
Refresh, and what happens to thieves
POST /auth/refresh
Content-Type: application/json
{ "refreshToken": "srt_…" }
Every refresh rotates: the old token is atomically marked used, a new srt_ token is
issued in the same family. Present a rotated token twice and Sentinel revokes the entire
family and emits token.refresh_reuse_detected — the thief and the victim both lose the
session, and the victim’s next login is the alarm. The
token theft recipe walks through the
mechanism and the tests that pin it.
The authenticated call
GET /profile/me
Authorization: Bearer eyJ…
The Sentinel authentication handler establishes a SentinelPrincipal from — in
order — the Authorization: Bearer header, the access-token cookie, or an snt_ API
key. Your handlers read it via HttpContext.GetSentinelPrincipal() or the injected
ISentinelContextAccessor:
app.MapGet("/reports", (HttpContext http) =>
{
var principal = http.GetSentinelPrincipal(); // SubjectId, RealmId, OrganizationId,
// SessionId, Kind, ImpersonatorId …
var snapshot = http.GetSentinelSnapshot(); // the permission snapshot
// …
});
Where to go next
- Article 001 — First login walks this same path end to end with the full request/response transcript.
- Authentication & factors adds passkeys, TOTP and risk-based step-up.
- Authorization engine introduces grants, the permission grammar
and
AuthorizationEvaluator. - Deployment covers running the prebuilt Sentinel Server instead of embedding.
Learn by building
The tutorials for this area, in order — each with a runnable sample.