Authentication and accounts
WebLogic implements the flows: password sign-in with lockout and throttling, passkeys (guide), TOTP two-factor with replay protection, recovery codes, password reset, e-mail verification and change, magic links, API keys, bearer tokens, external providers and tenants. Storage is yours: you implement the store contracts and WebLogic calls them.
Stores
| Contract | Holds | Register with |
|---|---|---|
IWebUserStore |
Users (WebUser): name, e-mail, password hash, security stamp, groups, permissions, tenant, 2FA secret, lockout |
web.UseUserStore<T>() |
IWebUserLockoutStore, IWebUserEmailStore, IWebUserExternalLoginStore |
Optional capabilities on the same class: atomic failure counting, lookup by e-mail, linked external logins | (implement on the user store) |
IWebSessionStore |
Session records keyed by the SHA-256 of the session token | web.UseSessionStore<T>() |
IWebApiKeyStore |
API keys (hash of the secret, scopes, expiry, networks) | web.UseApiKeyStore<T>() |
IWebPasskeyStore |
Passkeys (public key, user handle, counter); see Passkeys | web.UsePasskeyStore<T>() |
IWebRecoveryCodeStore |
Hashed recovery codes; TryConsumeAsync must be atomic |
web.UseRecoveryCodeStore<T>() |
IWebOneTimeUseStore |
Spent TOTP steps and single-use links (in memory by default) | web.UseOneTimeUseStore<T>() |
IWebLoginThrottle |
Back-off per login, cap per client (in memory by default) | web.UseLoginThrottle<T>() |
IWebSecurityNotifier |
Sends security e-mails | web.UseMailNotifications() (CL.Mail) or your own |
Each Use…<T>() registers T as one singleton for its own type and the contract, so the app can
inject its store by type (for its extra methods) and gets the instance WebLogic uses.
web.UseInMemoryAccounts() registers in-memory users, sessions, keys and codes for development and
tests. Outside Development the app refuses to start while any of them is still in memory. The
samples have complete stores: JSON files in samples/StarterWebsite/Infrastructure; MiniBlog uses
the CodeLogic.WebLogic.MySql add-on.
Signing in
var result = await request.Auth.PasswordSignInAsync(model.Login, model.Password, persistent: model.RememberMe, ct);
switch (result.Status)
{
case WebSignInStatus.Succeeded: return FormResult.Success(redirect: WebRequestAuth.GetSafeReturnUrl(model.ReturnUrl));
case WebSignInStatus.RequiresTwoFactor: return FormResult.Success(redirect: "/account/two-factor");
case WebSignInStatus.RequiresPasswordChange: /* result.PasswordChangeToken → CompletePasswordChangeAsync */ break;
case WebSignInStatus.LockedOut or WebSignInStatus.Throttled: /* result.RetryAfter */ break;
default: /* InvalidCredentials, NotAllowed */ break;
}
- Unknown users still cost a full hash, so timing does not reveal which logins exist; a locked account is only reported to someone who typed the right password.
- One attempt per login (and per pending second factor) runs at a time on a node, so parallel bursts cannot get around the throttle.
- Sign-in rotates the session id and the CSRF secret.
GetSafeReturnUrlaccepts local paths only (no//,/\, schemes or control characters).
Other request.Auth members: CompleteTwoFactorAsync(code), RedeemRecoveryCodeAsync(code),
SignInWithMagicLinkAsync(token), SignInExternalAsync(user, provider),
CompletePasswordChangeAsync(token, password) (a refused password keeps the token and sets
result.Error to the policy message, safe to show), ReauthenticateAsync(password, code),
SignInAsync(user) (after your own checks; skips the second factor), SignOutAsync(everywhere),
RefreshAsync(), ChangePasswordAsync(current, new), ListSessionsAsync(),
RevokeSessionAsync(idHash) and GetCurrentUserAsync().
Remember me
Every sign-in method takes persistent ("remember me"). It carries through the second factor and a
required password change. Remembered sessions use Session.PersistentIdleTimeoutDays (30) and
Session.PersistentAbsoluteTimeoutDays (90) and a cookie that survives a browser restart; other
sessions end with the browser session (or the normal idle/absolute timeouts). Security-stamp checks,
rotation and "sign out everywhere" apply to both.
Step-up (recent sign-in)
Sensitive pages can demand that the user proved their credentials recently, optionally with specific methods:
account.MapPage("/delete", DeleteAccountAsync).RequireRecentSignIn(TimeSpan.FromMinutes(10));
b.MapGroup("/billing").RequireRecentSignIn(TimeSpan.FromMinutes(15), "pwd", "otp");
web.AddAuthorizationPolicy("payouts", p => p.RequireAnyGroup("finance").RequireRecentSignIn(TimeSpan.FromMinutes(5), "otp"));
When it fails, pages redirect to the login path with reauth=1&returnUrl=… and APIs answer 401
with WWW-Authenticate: WebLogic error="reauthentication_required". Users prove themselves with
request.Auth.ReauthenticateAsync(password, code) (the code is required when they use two-factor
authentication), ReauthenticateWithPasskeyAsync (Passkeys) or by simply signing in
again; refreshing the session does not count. Users without a password sign in again through their
magic link, provider or passkey.
The listed methods must all have been used in the latest sign-in. The method names are pwd,
otp, recovery, passkey, link and ext:{provider}. A passkey sign-in records only passkey,
so "pwd", "otp" above sends passkey users to prove themselves again with a password and code.
To accept several ways of proving it, give alternatives; each is a set that must be complete:
b.MapGroup("/admin").RequireRecentSignInAnyOf(TimeSpan.FromHours(8), ["pwd", "otp"], ["passkey"]);
web.AddAuthorizationPolicy("payouts", p => p.RequireRecentSignInAnyOf(TimeSpan.FromMinutes(5), ["otp"], ["passkey"]));
Sessions as devices
var sessions = await request.Auth.ListSessionsAsync(ct); // browser, created, last seen, remembered, IsCurrent
await request.Auth.RevokeSessionAsync(sessions[1].IdHash, ct); // "sign out that device"
Session records keep the browser's user agent, the authentication time and "remember me". Listing
needs a session store that implements the optional IWebSessionListStore
(ListForUserAsync(userId)); both samples do. The same capability enables:
Session.MaxPerUser: the oldest sessions end when a new sign-in exceeds the cap.Auth.Notifications.NotifyNewDevice: an e-mail when someone signs in from a browser none of the user's other sessions use. Browsers are compared by browser and system family ("Chrome on Windows",WebUserAgent), so a browser update is not a new device; the e-mail andWebSessionInfo.Devicecarry that name. An unrecognised user agent is named "an unknown browser": the raw header, which the client controls, never goes into the e-mail (thedevicetemplate value).- Ending a session (
RevokeAsync, the per-user cap, a password reset) also closes its realtime connections: at once on the same node, and withinRealtime.SessionCheckSecondson the others.
Passwords
Pbkdf2PasswordHasher: PBKDF2-SHA256, 600,000 iterations, per-hash salt, optional pepper
(auth.passwords.pepper, keep it with your database backups), versioned format
($wl$pbkdf2-sha256$v=1$...). Hashes are upgraded on sign-in when the settings change. Plug in other
algorithms with web.AddPasswordAlgorithm<T>(). The default IWebPasswordPolicy checks length and
refuses passwords that contain the user name or e-mail.
Two-factor and recovery codes
var enrollment = twoFactor.BeginEnrollment(user); // secret, otpauth URI, QR code, enrollment token
// ... user scans the QR code and types the first code ...
var codes = await twoFactor.ConfirmEnrollmentAsync(user, enrollment.EnrollmentToken, code, ct);
await request.Auth.RefreshAsync(ct); // ends the user's other sessions
TOTP codes come from CL.TwoFactorAuth; each time step is accepted once (IWebOneTimeUseStore).
Secrets are stored DataProtection-protected, so persist your key ring. Recovery codes are shown once,
stored hashed and consumed atomically.
Suspending accounts
Set WebUser.SuspendedUntil (and SuspensionReason) to ban a user for a while, or with
DateTimeOffset.MaxValue until lifted. Unlike IsDisabled, the user is told why:
- Sign-in methods return
WebSignInStatus.Suspendedwithresult.Suspension(until, reason), but only after the password, link or provider proved the user, so it tells a password guesser nothing. - Existing sessions end at their next security-stamp check (
auth.securityStampCheckMinutes; change the stamp as well to end them at once), and the user's API keys stop working. WebResult.AccountSuspended(result.Suspension)answers 403 with the theme'serrors/suspendedtemplate (model:reason,until,indefinite), a plain built-in page without one, and ProblemDetails (suspendedUntil) for API clients.
Your user store persists the two fields like any other (the samples show JSON and MySQL).
Links by e-mail
WebAccountService covers registration, ConfirmEmailAsync, RequestPasswordResetAsync/ResetPasswordAsync,
RequestEmailChangeAsync/ConfirmEmailChangeAsync and RequestMagicLinkAsync. Tokens are
DataProtection-protected, purpose-scoped, time-limited, bound to the user's security stamp and
single-use. Request methods answer the same whether or not an account exists, mail is sent in the
background, and each address gets at most a few link e-mails per 15 minutes. A magic link that
confirms a never-confirmed address also drops any password set at registration (against account
pre-hijacking). Changing password, e-mail or 2FA changes the security stamp, which ends the user's
other sessions at their next check (auth.securityStampCheckMinutes).
API keys
var created = await apiKeys.CreateAsync(new WebApiKeyRequest
{
Name = "CI deploy", UserId = user.Id, Scopes = ["deploy"], Lifetime = TimeSpan.FromDays(90)
}, ct);
// created.PlainTextKey ("wl_<prefix>_<secret>") is shown once.
Sent as X-Api-Key, Authorization: ApiKey ... or Authorization: Bearer wl_.... Only the SHA-256
of the secret is stored; comparison is constant-time. Keys can be limited by scope, expiry, networks
(CIDRs), tenant and their own rate-limit policy, and stop working when revoked or when their owner is
disabled or deleted. API-key requests are not "ambient", so they need no CSRF token.
Bearer tokens, ASP.NET schemes, trusted headers
web.AddBearerTokens<TValidator>(): you validate the token (JWT or opaque) inIWebBearerTokenValidatorand return aWebIdentity.web.AddAspNetAuthenticationScheme("Negotiate", ambient: true): reuse an ASP.NET handler (Windows auth, JwtBearer, ...).auth.trustedHeader: identity from a fronting proxy such as oauth2-proxy, accepted only from a trusted proxy address and with the shared secret header.
External providers
Implement IWebExternalLoginProvider (GetChallengeUrlAsync, HandleCallbackAsync) and register it
with web.AddExternalLoginProvider<T>(). WebExternalLoginFlow handles state and nonce (in a
protected __Host- cookie), the safe return URL and linking. Finish with
request.Auth.SignInExternalAsync(user, provider): it refuses disabled, locked and other-tenant
accounts and asks users with two-factor authentication for their code, exactly like a password
sign-in. (SignInAsync would skip the second factor.)
Ready-made providers (Steam, Google, Microsoft, GitHub, Discord and any OpenID Connect issuer, with
PKCE and in-house ID token validation) are in the
CodeLogic.WebLogic.ExternalLogins add-on; the starter sample uses it.
Tenants
web.UseHostTenants(new Dictionary<string, string> { ["a.example.com"] = "a", ["b.example.com"] = "b" })
or your own IWebTenantResolver. The tenant flows into the identity, rate-limit partitions, output
cache keys, realtime delivery, API-key checks and RequireSameTenant(). Users with a tenant cannot
sign in on another tenant.