AuthServer
Password Login
Authenticate users with email and password.
Password login uses email + password. Run home realm discovery first. It tells you whether an email must go to SSO before you show any local credential form. The same rule applies when Email OTP is enabled.
Given an email address, discovery returns the login mode:
var discovery = await discoveryService.DiscoverAsync(
new SqlOSHomeRealmDiscoveryRequest("user@acme.com"), ct);
// discovery.Mode → "password" or "sso"| Mode | Meaning |
|---|---|
password | No SSO org for this email — show a local credential method such as password or Email OTP |
sso | Domain maps to SAML — redirect to IdP |
Frontend example (Next.js):
const res = await fetch("/api/v1/auth/discover", {
method: "POST",
body: JSON.stringify({ email }),
});
const { mode, organizationId } = await res.json();
if (mode === "sso") {
// Start SSO flow
} else {
// Show password input
}var result = await authService.LoginWithPasswordAsync(
new SqlOSPasswordLoginRequest(email, password, clientId, organizationId: null),
httpContext, ct);
if (result.RequiresOrganizationSelection)
{
// User belongs to multiple orgs
// Present result.Organizations, then:
var tokens = await authService.SelectOrganizationAsync(
new SqlOSSelectOrganizationRequest(result.PendingAuthToken, selectedOrgId),
httpContext, ct);
}
else
{
var accessToken = result.Tokens.AccessToken;
var refreshToken = result.Tokens.RefreshToken;
}
A typical frontend flow using the example API endpoints:
// 1. Discover login mode
const discover = await apiPost("/api/v1/auth/discover", { email });
// 2. Login with password
const login = await apiPost("/api/v1/auth/login", {
email,
password,
organizationId: discover.organizationId,
});
if (login.requiresOrganizationSelection) {
// Show org picker, then:
const tokens = await apiPost("/api/v1/auth/select-organization", {
pendingAuthToken: login.pendingAuthToken,
organizationId: selectedOrgId,
});
}
// 3. Store tokens
localStorage.setItem("access_token", login.accessToken);Hosted AuthPage includes a Forgot password? action when local password auth is enabled. It sends a branded reset email and opens the built-in reset form at /sqlos/auth/password/reset?token=....
Headless and API-owned UIs can request the same email without exposing the token:
POST /sqlos/auth/password/forgot
POST /sqlos/auth/headless/password/forgotBoth routes return a generic success response for unknown, inactive, SSO-only, and otherwise ineligible accounts. Only active users with an existing local password credential receive email.
await authService.RequestPasswordResetEmailAsync(
new SqlOSSendPasswordResetEmailRequest(
Email: email,
ResetUrlTemplate: null,
ClientId: clientId),
httpContext,
ct);Support teams can send the same email from the dashboard user detail page or by calling:
POST /sqlos/admin/auth/api/users/{userId}/password-reset-emailDisable password auth entirely:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.EnableLocalPasswordAuth = false;
});Require verified email before password login:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.RequireVerifiedEmailForPasswordLogin = true;
});Configure password-login abuse controls:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.ConfigurePasswordLoginAbuse(password =>
{
password.MaxFailedAttemptsPerAccount = 5;
password.MaxFailedAttemptsPerIp = 20;
password.MaxFailedAttemptsPerClient = 50;
password.FailureWindow = TimeSpan.FromMinutes(15);
password.LockoutDuration = TimeSpan.FromMinutes(15);
});
});SqlOS stores password-login throttle state in SqlOSPasswordLoginBuckets. Failed password attempts count against the normalized email, user ID when known, source IP, client, and user-agent fingerprint. Account buckets reset after a successful password login. IP, client, and device buckets expire through the configured failure window so one successful login cannot clear a password-spray pattern.
Unknown-email, wrong-password, locked-account, and rate-limited password attempts return the same public failure message. Operators can distinguish them internally through SqlOSAuditEvents entries such as password.login.failed, password.login.locked, password.login.rate_limit_rejected, password.login.suspicious_pattern, and password.login.succeeded.
Configure password-reset lifetime and request limits:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.ConfigurePasswordReset(reset =>
{
reset.TokenLifetime = TimeSpan.FromHours(1);
reset.ResendCooldown = TimeSpan.FromSeconds(30);
reset.MaxRequestsPerEmailPerWindow = 5;
reset.MaxRequestsPerIpPerWindow = 60;
reset.MaxRequestsPerClientPerWindow = 300;
});
});Password-reset requests are audited with events such as password_reset.requested, password_reset.email_sent, password_reset.email_send_failed, password_reset.rate_limit_rejected, password_reset.completed, and password_reset.invalid_or_expired.