Skip to main content
Version: Current

Advanced Configuration

This page covers scenarios that go beyond the defaults: custom user stores, cluster deployments, and built-in admin impersonation.

1. Class-Based User Store

If you need constructor-injected dependencies (a database context, a logger, etc.), implement IPrimusAuthUserStore as a class. For a simpler lambda-based approach that does not need DI, use .WithAutoProvision() instead (see Section 2).

public class AppUserStore : IPrimusAuthUserStore
{
private readonly AppDbContext _db;
public AppUserStore(AppDbContext db) => _db = db;

public async Task<PrimusAuthUser?> FindByEmailAsync(string email, CancellationToken ct = default)
{
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email, ct);
if (user == null) return null;
// Role populates the ClaimTypes.Role claim — used by [Authorize(Roles = "Admin")]
return new PrimusAuthUser { Id = user.Id.ToString(), Email = user.Email, Role = user.Role };
}

// Called on first SSO login — create the user record and return it.
// Return null to block unknown SSO accounts. Safe to return null for local-only apps.
public async Task<PrimusAuthUser?> AutoProvisionUserAsync(
string email, string provider, ClaimsPrincipal principal, CancellationToken ct = default)
{
var newUser = new User { Email = email, Source = provider };
_db.Users.Add(newUser);
await _db.SaveChangesAsync(ct);
return new PrimusAuthUser { Id = newUser.Id.ToString(), Email = newUser.Email };
}

public Task<bool> IsLockedOutAsync(string email, CancellationToken ct) => Task.FromResult(false);
public Task RecordLoginFailureAsync(string email, CancellationToken ct) => Task.CompletedTask;
public Task ResetLoginFailureAsync(string email, CancellationToken ct) => Task.CompletedTask;
}

Register it before AddPrimusAuthBroker. The broker uses TryAdd internally, so a store registered first takes precedence:

builder.Services.AddScoped<IPrimusAuthUserStore, AppUserStore>();
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment());
Development vs production
  • isDevelopment: true — auto-registers InMemoryPrimusAuthUserStore if no store is found. Login works immediately without a database — useful for prototyping.
  • isDevelopment: false — registers NullPrimusAuthUserStore, which blocks all logins and logs a Critical startup warning until you register your own store.

2. JIT Auto-Provisioning (SSO only)

WithAutoProvision is the simplest way to handle SSO user creation without a full class. checkUser is called on every SSO login; autoProvision is called only when checkUser returns null (i.e., the user does not exist yet).

builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment())
.WithAutoProvision(
checkUser: async (email, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email);
return user == null ? null : new PrimusAuthUser { Id = user.Id, Email = user.Email, Role = user.Role };
},
autoProvision: async (email, provider, principal, sp) =>
{
var db = sp.GetRequiredService<AppDbContext>();
var newUser = new User { Email = email, Source = provider, Role = "User" };
db.Users.Add(newUser);
await db.SaveChangesAsync();
return new PrimusAuthUser { Id = newUser.Id.ToString(), Email = email, Role = "User" };
});

If checkUser returns null and WithAutoProvision is not configured, the SSO login is rejected and the user sees the configured ErrorRedirect.

3. Custom Credential Validator

Only needed if you use the /api/auth/login endpoint (email + password). Not required for SSO-only setups.

public class AppCredentialValidator : IPrimusAuthCredentialValidator
{
private readonly AppDbContext _db;
public AppCredentialValidator(AppDbContext db) => _db = db;

public async Task<PrimusAuthUser?> ValidateCredentialsAsync(string email, string password, CancellationToken ct)
{
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
if (user != null && VerifyHash(password, user.PasswordHash))
return new PrimusAuthUser { Id = user.Id, Email = user.Email };
return null;
}
}

Register it before AddPrimusAuthBroker:

builder.Services.AddScoped<IPrimusAuthCredentialValidator, AppCredentialValidator>();
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment());

4. High Availability (Cluster / Kubernetes)

Two things need to be shared across all replicas: session cookie keys and the rate limiter state.

Share Data Protection Keys — so a cookie encrypted by pod A can be read by pod B:

// Point all replicas at a shared volume (Azure Files, EFS, etc.)
builder.Services.AddPrimusBrokerDataProtection("PrimusApp", new DirectoryInfo(@"/mnt/shared/keys"));
The default rate limiter is not cluster-safe

InMemoryRateLimiter stores counters in process memory. Each pod maintains its own counter independently, so the effective limit across the cluster is maxRequests × pod count. To enforce a consistent global limit, register a distributed IPrimusAuthRateLimiter (e.g. Redis-backed) before AddPrimusAuthBroker:

builder.Services.AddSingleton<IPrimusAuthRateLimiter, MyRedisRateLimiter>();
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment());

5. Admin Impersonation

Admins can log in as any user to reproduce bugs without knowing their password.

// Impersonate (Admin role required)
await fetch('/api/auth/impersonate', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Primus-CSRF': getCsrfToken(),
},
body: JSON.stringify({ targetEmail: 'user@example.com' }),
});

// End impersonation
await fetch('/api/auth/revert', {
method: 'POST',
credentials: 'include',
headers: { 'X-Primus-CSRF': getCsrfToken() },
});
EndpointRole
POST /api/auth/impersonateAdmin or SuperAdmin
POST /api/auth/revertAny (with an active impersonation session)

Both actions are recorded by IPrimusAuthAuditSink as ImpersonateStart and ImpersonateEnd.

Revert signs you out

POST /api/auth/revert clears your session entirely — it does not restore your original admin session. You must log in again after reverting. This is intentional to avoid storing multiple concurrent session states.


Next Steps

  • Integration Guide — complete setup walkthrough from install to first authenticated request
  • Endpoint Reference — all impersonation, passkey, and admin endpoints with full request and response shapes
  • Database Setup — persist users with EF Core and a full list of built-in implementations
  • Bot Protection & Rate Limiting — Redis-backed distributed rate limiting for multi-instance deployments