Guides
Require Authenticator MFA
Enable TOTP, let users self-enroll, and require MFA for a tenant.
This guide enables authenticator-app MFA and tests both voluntary enrollment and forced organization policy.
You will configure SqlOS so:
Keep the first version optional unless your product needs an immediate tenant-wide requirement.
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.ConfigureMfa(mfa =>
{
mfa.Enabled = true;
mfa.AllowUserSelfEnrollmentByDefault = true;
mfa.RecoveryCodesEnabledByDefault = true;
mfa.RequireForAllUsersByDefault = false;
mfa.Totp.Issuer = "Acme";
});
});To let configuration own the value across restarts, seed the policy:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.SeedMfaPolicy(mfa =>
{
mfa.Enabled = true;
mfa.TotpEnabled = true;
mfa.UserSelfEnrollmentEnabled = true;
mfa.RecoveryCodesEnabled = true;
mfa.RequireForAllUsers = false;
});
});Open /sqlos/admin/auth/ and go to MFA.
Confirm:
If the screen says the values are startup managed, changes made in the dashboard are overwritten on restart.
Use the organization policy API when one tenant requires MFA:
curl -X PUT http://localhost:5062/sqlos/admin/auth/api/organizations/org_123/mfa-policy \
-H "Content-Type: application/json" \
-d '{
"isEnabled": true,
"requireMfaForAllUsers": true,
"requireMfaForOwnersAndAdmins": false,
"userSelfEnrollmentEnabled": true,
"recoveryCodesEnabled": true,
"requiredRoles": ["owner", "admin"],
"availableFactors": ["totp", "recovery_code"]
}'Use requireMfaForOwnersAndAdmins instead when only privileged tenant roles need MFA.
Expose protected app endpoints that call SqlOSAuthService.
app.MapGet("/api/mfa/status", async (
SqlOSAuthService authService,
ClaimsPrincipal user,
CancellationToken ct) =>
{
var userId = user.FindFirstValue("sub")!;
var organizationId = user.FindFirstValue("org_id");
return await authService.GetMfaStatusAsync(userId, organizationId, ct);
});
app.MapPost("/api/mfa/totp/enroll/start", async (
SqlOSTotpEnrollmentStartRequest request,
SqlOSAuthService authService,
ClaimsPrincipal user,
CancellationToken ct) =>
{
var userId = user.FindFirstValue("sub")!;
var organizationId = user.FindFirstValue("org_id");
return await authService.StartTotpEnrollmentAsync(userId, request, organizationId, ct);
});
app.MapPost("/api/mfa/totp/enroll/verify", async (
SqlOSTotpEnrollmentVerifyRequest request,
SqlOSAuthService authService,
HttpContext httpContext,
CancellationToken ct) =>
await authService.VerifyTotpEnrollmentAsync(request, httpContext, ct));Render qrCodeDataUrl as an image, keep secret available for manual setup, and verify the 6-digit TOTP code with the enrollment token. Show recovery codes once after verification.
Hosted auth handles this automatically.
For headless auth, render these view names:
| View | UI |
|---|---|
mfa | Code input for authenticator code or recovery code |
mfa-enroll | QR code, manual setup secret, and TOTP verification input |
Post the challenge code:
POST /sqlos/auth/headless/mfa/verify
{
"requestId": "req_123",
"mfaToken": "tmp_...",
"code": "123456"
}Post forced enrollment verification:
POST /sqlos/auth/headless/mfa/totp/enroll/verify
{
"requestId": "req_123",
"mfaToken": "tmp_...",
"enrollmentToken": "tmp_...",
"code": "123456"
}The example stack includes both optional self-enrollment and a seeded Retail org that requires MFA.
dotnet run --project examples/SqlOS.Example.AppHost/SqlOS.Example.AppHost.csprojThen:
http://localhost:3010./retail/account.Two-step verification.admin@retail.demo with RetailDemo1!. Expect forced authenticator enrollment before entering the app.requiresMfa or requiresMfaEnrollment as the next auth state and post the MFA step back to SqlOS. Tokens are intentionally absent until SqlOS completes the second factor.mfa and mfa-enroll in headless UIs.