CodeLogic.WebLogic.Testing
4.9.8-preview
See the version list below for details.
dotnet add package CodeLogic.WebLogic.Testing --version 4.9.8-preview
NuGet\Install-Package CodeLogic.WebLogic.Testing -Version 4.9.8-preview
<PackageReference Include="CodeLogic.WebLogic.Testing" Version="4.9.8-preview" />
<PackageVersion Include="CodeLogic.WebLogic.Testing" Version="4.9.8-preview" />
<PackageReference Include="CodeLogic.WebLogic.Testing" />
paket add CodeLogic.WebLogic.Testing --version 4.9.8-preview
#r "nuget: CodeLogic.WebLogic.Testing, 4.9.8-preview"
#:package CodeLogic.WebLogic.Testing@4.9.8-preview
#addin nuget:?package=CodeLogic.WebLogic.Testing&version=4.9.8-preview&prerelease
#tool nuget:?package=CodeLogic.WebLogic.Testing&version=4.9.8-preview&prerelease
CodeLogic.WebLogic.Testing
Test helpers for apps built on CodeLogic.WebLogic.
WebLogicTestHost runs your WebLogic setup on a real Kestrel server bound to 127.0.0.1:0 with a
cookie-aware HttpClient, so tests go through the full pipeline — security stages, CSRF, sessions,
output cache — exactly like production requests. No ASP.NET TestHost package needed.
using CL.WebLogic.Testing.Services;
[Fact]
public async Task Anonymous_post_without_a_csrf_token_is_refused()
{
await using var host = await WebLogicTestHost.StartAsync(new WebLogicTestHostOptions
{
ConfigureWebLogic = web =>
{
web.UseInMemoryAccounts();
web.AddContributor(new DelegateContributor(b =>
b.MapPage("/contact", (r, _) => ValueTask.FromResult<WebResult>(WebResult.Text("ok")), "POST").AllowAnonymous()));
}
});
var response = await host.Client.PostAsync("/contact", new FormUrlEncodedContent([]));
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
What's inside
WebLogicTestHost/WebLogicTestHostOptions: config, environment (test production behaviour), your WebLogic registrations, extra services and middleware; theme files viaWriteContentFile.DelegateContributor: map routes inline in a test.TemplateTestRenderer+InMemoryTemplateLoader: render templates without a host.ManualTimeProvider: control time for lockouts, token expiry and sessions.WebTestAuthenticator: a software passkey authenticator; turns your passkey endpoints' options into the JSON a browser posts back, with real keys, signatures and attestation (none, packed, fido-u2f).FakeWebLogicContext+RecordingLogger: assert on what the toolkit logged.
Works with any test framework. No third-party dependencies beyond CodeLogic.*.
Docs: https://zyntal-com.github.io/CL.WebLogic2/guides/testing.html
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CodeLogic.WebLogic (>= 4.9.8-preview)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.9.9-preview | 0 | 10/27/2026 |
| 4.9.8-preview | 0 | 10/27/2026 |
| 4.9.7-preview | 19 | 10/27/2026 |
| 4.9.6-preview | 21 | 10/27/2026 |
| 4.9.5-preview | 26 | 10/26/2026 |
| 4.9.4-preview | 19 | 10/26/2026 |
| 4.9.3-preview | 20 | 10/26/2026 |
| 4.9.2-preview | 25 | 10/26/2026 |
| 4.9.1-preview | 28 | 10/25/2026 |
# Changelog
## 4.9.8-preview (2026-10-27)
### Fixed
- **Form binding matched posted names case-sensitively.** A field named after the property (`Image`,
`Email`) did not bind from a client posting `image` or `email`: the field stayed empty, so a
`[Required]` field failed and an optional one was silently ignored. Posted names now match
case-insensitively, in `BindAsync`, remote field validation, wizard steps and repeater items (file
fields already did). If a name is posted twice in different case, the values are merged in posted
order and a single-value field takes the first, instead of the post failing. The internal `__wl_*`
and CSRF fields are recognised in any case too.
### Changed
- A form model whose fields differ only in case (`Name` and `name`) is refused when it is registered
(`InvalidOperationException`), because those fields could no longer be told apart.
## 4.9.7-preview (2026-10-27)
### Added
- `WebHtmlSanitizer`: opt-in `style` declarations and embed iframes, for rich text that resizes images or
embeds videos.
- `WebHtmlSanitizerOptions.AllowedStyleProperties`: property name → value check. The attribute is
rebuilt from the allowed declarations only. Values with comments, escapes, quotes, `url()` or any
function other than `rgb()`/`rgba()`/`hsl()`/`hsla()` are refused before the check, as are
unknown properties; `!important` is stripped.
- `WebCssValues`: `Length`, `Percentage`, `Color`, `OneOf(...)`, `Any(...)` value checks.
- `WebHtmlSanitizerOptions.AllowedFrameSources` (`WebFrameSource(host, pathPrefix)`): https only,
exact host, default port, no user info. A kept iframe is rebuilt with `src`, `width`, `height`,
`title`, `loading` and the global attributes, plus `FrameSandbox`, `FrameAllow`, `allowfullscreen`
and `referrerpolicy="strict-origin-when-cross-origin"`; its fallback content is dropped.
- Defaults are unchanged: no `style` attributes, no iframes. Add the frame hosts to the CSP
(`security.headers.frameSources`) as well.
## 4.9.6-preview (2026-10-27)
### Security
- New-device e-mails no longer carry the raw `User-Agent` header. When the browser was not
recognised, the header (chosen by the client) went into the mail as the `device` value, and a CL.Mail
HTML template using it could be injected into. Unrecognised browsers are now named "an unknown
browser".
### Added
- `WebSignInResult.Error`: when `CompletePasswordChangeAsync` refuses the new password, the result keeps
the token and carries the password policy's message ("Use at least 10 characters.", "The password
must not contain your user name or e-mail."), so the page can say why.
### Changed
- `UseUserStore<T>()` and the other `Use…<T>()` registrations add `T` as a singleton of its own type and
resolve the contract to that same instance. Apps can inject their store by type (for app-specific
methods) without casting `IWebUserStore`, and a class registered for several contracts is one instance.
## 4.9.5-preview (2026-10-26)
### Added
- Step-up with alternatives: `RequireRecentSignInAnyOf(maxAge, ["pwd", "otp"], ["passkey"])` on routes,
groups and policies accepts any one complete set of methods. `RecentSignInRequirement.Alternatives`
replaces `Methods`, and `WebRouteSettings.RecentSignInAlternatives` replaces `RecentSignInMethods`.
- `WebUserAgent`: browser and system family of a user agent ("Chrome on Windows"), and
`WebSessionInfo.Device`.
- `Realtime.SessionCheckSeconds` (30) and `SessionEndedEvent`.
### Fixed
- A revoked session kept its realtime connection. Signing a device out, the per-user session cap and
password-reset sign-outs ended the session, but its SSE/WebSocket stream stayed open for up to
`MaxConnectionMinutes` (60) and kept receiving that user's messages. Connections now record their
session and close when it ends: at once on the same node, and within `SessionCheckSeconds` on others.
- New-device e-mails compared the exact user-agent string, so every browser update was mailed as a new
sign-in. They now compare browser and system family and name the device ("Chrome on Windows").
### Changed
- CodeLogic.WebLogic.MySql uses only CL.MySQL2's typed API (now CodeLogic.MySQL2 4.9.101): the add-on
contains no SQL text. Rate limits refill and charge a bucket with one atomic update-and-read, a
sign-in failure is one upsert, a fresh one-time key is one round trip, and recovery code sets are
replaced in one transaction.
## 4.9.4-preview (2026-10-26)
### Added
- Passkeys (WebAuthn), verified in-house: CBOR, COSE keys (ES256/384/512, RS256, PS256), authenticator
data, signatures, and the `none`, `packed` and `fido-u2f` attestation formats.
- **Three uses:** passwordless sign-in (`request.Auth.PasskeySignInAsync`, multi-factor, so no TOTP
step), the second factor after a password (`CompleteTwoFactorWithPasskeyAsync`), and step-up
(`ReauthenticateWithPasskeyAsync`).
- **Registration and management:** `BeginPasskeyRegistrationAsync` and `CompletePasskeyRegistrationAsync`,
plus list, rename and delete. Users are e-mailed when a passkey is added or removed.
- **Safety:** ceremony state is sealed and each challenge is single-use (works across nodes).
Origin, RP id, user presence and verification, backup flags and the user handle are checked.
A signature counter that goes backwards refuses the sign-in (`PasskeyCloneSuspectedEvent`).
- **Lockout:** the password lockout neither blocks passkeys nor counts passkey failures, because
credential ids are not secret. Failures are throttled instead.
- **Wiring:** `IWebPasskeyStore` (`web.UsePasskeyStore<T>()`, an in-memory store with
`UseInMemoryAccounts`) and `Auth.Passkeys` configuration (RP id, origins, timeout, attestation,
second factor without PIN, maximum per user).
- Client: `WebLogic.passkeys` (`signIn`, `register`, `verify`, passkey autofill) and declarative
`data-wl-passkey` buttons.
- Testing: `WebTestAuthenticator`, a software authenticator for testing passkey endpoints. The suite
also runs the client against a headless-Chrome virtual authenticator.
- CodeLogic.WebLogic.MySql: `wl_passkeys` (`WebMySqlStores.Passkeys`, included in `All`), with an
atomic counter update.
- Starter sample: passkey sign-in with autofill, passkey as the second factor, and a Passkeys card on
the account page.
### Fixed
- A suspended user's reason and end date were lost on the way back from password, magic-link and
external sign-in (`WebSignInResult.Suspension` was null), so the suspension page could not show them.
## 4.9.3-preview (2026-10-26)
### Added
- App-managed IP rules and blocklists: `IWebIpRuleProvider` (`web.AddIpRuleProvider<T>()`) returns
`WebIpRule`s (deny, allow or exempt; network or country; expiry; reason) merged with
`Security.IpRules`, and `IWebReputationListProvider` returns DNSBL zones that replace the CL.NetUtils
services. Cached for `IpRules.ProviderCacheSeconds` (60); `IWebSecurityRulesInvalidator.Invalidate()`
applies edits at once; a failing provider keeps the last good rules. Exempt addresses skip the
allow-list, country rules and the DNSBL check.
- `WebSecurityRejectedEvent.RuleId` (and `WebSecurityDecision.RuleId`): the matched rule
(`ban-42`, `config.deny[0]`, `dnsbl:{zone}`) for hit counters, never the client address.
- DataProtection key ring in CL.Storage: `AddDataProtection().PersistKeysToCodeLogicStorage(connectionId, path)`
(one file per key, never overwritten, storage errors fail instead of minting new keys), and
`ProtectKeysWithSecret(variable, previousVariables...)` to encrypt keys at rest with AES-256-GCM
from an environment secret, with rotation. Cluster checks accept the storage repository and warn
about a plain folder.
- Plugin assets: `b.MapAssets(assembly, "assets/", name)` serves embedded files at
`/_weblogic/plugins/{name}/...` (ETag, extension allowlist, path checks, removed with the plugin);
`{asset:name:path}` gives a fingerprinted URL, and a theme file at `assets/plugins/{name}/{path}`
overrides it.
- Group layouts: `WithLayout("admin/layout")` on routes and groups, used by page templates that
declare no `{layout:...}` (`TemplateRenderRequest.DefaultLayout`).
- Account suspension: `WebUser.SuspendedUntil` and `SuspensionReason`. Sign-in returns
`WebSignInStatus.Suspended` with `WebSignInResult.Suspension` (only after the credentials were
proven); sessions end at the next stamp check and API keys stop working.
`WebResult.AccountSuspended(suspension)` renders the theme's `errors/suspended` page (plain fallback,
ProblemDetails for API clients). Both samples handle it; MiniBlog adds two user columns.
- Git theme credentials in `ThemeConfig`: `GitTokenEnvironmentVariable` (or the secret `GitToken`),
`GitUserName`, `GitSshKeyPath` and `GitSshPassphraseEnvironmentVariable`.
- `WebThemeChecker.Check(folder)` lists templates that do not parse (file, line, column), and
`tools/CL.WebLogic.ThemeCheck` runs it from the command line. CI checks both sample themes.
- New package **CodeLogic.WebLogic.MySql**: production stores on CL.MySQL2 for every contract
(users with lockout, e-mail and external logins; sessions with device listing; API keys; recovery
codes; one-time keys; login throttle; rate limits; audit; widget settings; dashboards) and the
DataProtection key ring. `web.UseMySqlStores(connectionId, o => o.Stores = ...)`,
`AddDataProtection().PersistKeysToMySql(connectionId)`. Schema sync at startup, a background sweep,
and cross-node guarantees (single-use consumes, atomic lockout, exact rate-limit capacity, versioned
dashboards). Tested against MySQL 8.4 in CI.
- New package **CodeLogic.WebLogic.ExternalLogins**: `AddSteamLogin()` (moved from the starter
sample), `AddOAuthLogin(options)` for any OAuth 2.0 / OpenID Connect provider (authorization code +
PKCE S256, nonce, discovery with issuer check, in-house JWT validation for RS/PS/ES 256-512 with
JWKS rotation, `iss`/`aud`/`azp`/`exp`/`nbf`/`iat`, RFC 9207 `iss` check), and presets
`AddGoogleLogin`, `AddMicrosoftLogin`, `AddGitHubLogin`, `AddDiscordLogin`.
- `WebExternalChallenge.CodeVerifier` and `CodeChallenge`: `WebExternalLoginFlow` now creates a PKCE
verifier per sign-in and keeps it in its protected cookie.
### Changed
- Releases are manual only: the `promote.yml` workflow that merged `main` into `release` after green
tests is removed. Publish with `git push origin main:prerelease` or `main:release`.
- `IpRulesStage` and `ReputationStage` take a `WebSecurityRules` (resolved from DI).
- MiniBlog uses `CodeLogic.WebLogic.MySql` for its stores and key ring (encrypted when
`MINIBLOG_KEYRING_SECRET` is set); its own user, session, recovery-code, widget and dashboard
stores are gone. The starter sample uses `CodeLogic.WebLogic.ExternalLogins` (Steam, plus Google,
Microsoft, GitHub and Discord when `STARTER_{PROVIDER}_CLIENT_ID` and `_CLIENT_SECRET` are set).
## 4.9.2-preview (2026-10-26)
### Added
- `request.Auth.SignInExternalAsync(user, provider)`: sign-in for users an external provider vouched
for. Applies the account rules (disabled, locked, other tenant) and asks users with two-factor
authentication for their code, like a password sign-in. Recorded as `ext:{provider}`.
- Per-area sign-in pages: `WithLoginPath(path)` and `WithAccessDeniedPath(path)` on routes and
groups (inherited; local paths only). Signed-in users without access are sent to the denied page
instead of a 403 (pages only).
- Step-up: `RequireRecentSignIn(maxAge, methods...)` on routes and groups, and on policies
(`RecentSignInRequirement`). Failing it asks the user to sign in again (`reauth=1` redirect, or
`401` with `error="reauthentication_required"` for APIs) instead of forbidding.
`request.Auth.ReauthenticateAsync(password, code)` proves the user again; sessions and
`WebIdentity` record `AuthenticatedAt`.
- "Remember me": a `persistent` flag on every sign-in method, carried through the second factor
and a required password change. `Session.PersistentIdleTimeoutDays` (30) and
`Session.PersistentAbsoluteTimeoutDays` (90).
- Sessions as devices: records keep the user agent, authentication time and "remember me"; the
optional store capability `IWebSessionListStore` enables `request.Auth.ListSessionsAsync()`,
`RevokeSessionAsync(idHash)`, `Session.MaxPerUser` and new-device e-mails
(`Auth.Notifications.NotifyNewDevice`, notification kind `NewDeviceSignIn`). The in-memory store
and both samples implement it; the starter's account page lists signed-in devices.
- CSP: `MediaSources`, `WorkerSources`, `ManifestSources`, `FormActions` and `CspReportTo`
(`Reporting-Endpoints` + `report-to`). Per-route additions with `WithCsp(c => c.AddFrameSource(...))`
(validated: hosts, schemes and `'self'` only), built once per route.
- The explorer shows a route's login path, step-up requirement and CSP additions.
- `CodeLogic.WebLogic.Testing` ships a package readme (usage example and helper overview); the
`CodeLogic.WebLogic` readme gains a quick start and links to the docs.
### Changed (breaking)
- `request.Auth.PasswordSignInAsync`, `SignInAsync` and `SignInWithMagicLinkAsync` take
`persistent` before the cancellation token: callers passing the token positionally must name it
(`cancellationToken: ct`). The matching `WebAuthService` methods changed the same way.
- Sessions signed in without "remember me" now use a browser-session cookie (no `Expires`); before,
every session cookie lasted until the absolute timeout.
- Magic-link sign-in refuses users of another tenant, like password sign-in.
- `WebAuthService` takes a `WebSessionDevices` (resolved from DI).
### Fixed
- CI: golden template snapshots are found under deterministic (`/_/`) build paths, the Markdown
pathological-input test compares scaling instead of wall-clock time, and parallel Docker builds
lock the shared NuGet cache.
## 4.9.1-preview (2026-10-25)
First preview of the rebuild on nuget.org.
### Added
- New repository: CL.WebLogic rebuilt from the ground up on CodeLogic 4.9 and published as
`CodeLogic.WebLogic`. Breaking with the 0.x line — see `docs/guides/migrating.md`.
- `WebLogicLibrary` implements `IConfigReloadable`: `weblogic` config changes are applied live
through `WebLogicConfigHolder`; restart-only fields are flagged (`WebLogicRestartRequiredEvent`,
Degraded health) instead of failing the change.
- `IWebLogicContext` decouples toolkit services from the static CodeLogic runtime.
- `CodeLogic.WebLogic.Testing` package with `FakeWebLogicContext` and `RecordingLogger`.
- Hosting: `AddCodeLogicWebLogic()` / `UseCodeLogicWebLogic()` / `MapCodeLogicWebLogic()`. Every
WebLogic route is a real ASP.NET endpoint (templates and constraints such as `{id:int}`, 405 with
`Allow`, HEAD for GET routes, per-endpoint CORS) and mixes freely with minimal APIs.
- Routing: `IWebContributor.ContributeAsync(WebContributionBuilder, ct)` with `MapPage`, `MapApi`,
`MapFragment`, `MapSse`, `MapFallback` and nested `MapGroup` prefixes whose options (anonymous
access, groups, permissions, policies, CORS, rate limit, body limit, middleware) are inherited.
Routes are **secure by default**; anonymous routes are listed at registration.
- Contributions are all-or-nothing and removable: `WebContributionHandle`, `OnRemove` cleanups,
ambiguity detection, a single fallback. CodeLogic plugins implementing `IWebContributor` are
registered on load and removed on unload.
- Results: `Html`, `Text`, `Json` (per-contributor serializer options), `Bytes`, local-only
`Redirect` / explicit `ExternalRedirect`, `File` (ranges, ETag), `Problem` / `ValidationProblem`
(RFC 7807), `NoContent`, `Status`, `Commands` (allowlisted client commands), `Sse`, `FromAspNet`.
- `WebRequestContext` per request (no static accessor) with bounded body readers (413 above the
route limit), typed route values and cancellation on client disconnect.
- Exception boundary: correlation ids, safe 500 pages (details only in Development), ProblemDetails
for APIs, bodies for empty 4xx/5xx responses, client disconnects logged at Debug.
- Health probes `/_weblogic/health/live` and `/_weblogic/health/ready`.
- Theme assets served only from the theme's `assets` folder with path containment, dotfile and
symlink refusal, an extension allowlist, ETag / Last-Modified and immutable caching for `?v=` URLs.
- Templates: the AST template engine, ported and split into small types. Two engines share one set
of execution semantics (`TemplateOps`): `ClosureTemplateCompiler` (default; compiles a template
once into delegates) and `TemplateInterpreter` (the reference AST walk), selected with
`Theme.TemplateEngine`. The legacy regex engine is gone.
- Context-aware output encoding: the parser infers whether each value lands in element text, a
quoted attribute, a URL attribute (scheme allowlist: http, https, mailto, tel), a `style`
attribute, `<script>` or `<style>`. Unsafe positions (inside a tag, unquoted attributes, `on*`
handlers, script values without `|json`/`|js`) are template **parse errors**. New filters
`|json`, `|js`, `|attr`, `|css`, `|url`, `|markdown`, `|sanitize`, `|join`; `|nl2br` now encodes
first. `WebHtml` marks trusted markup.
- CSP nonces: `{nonce}`, and static `<script>` / `<style>` tags get a per-request nonce
automatically. New directives `{asset:path}` (fingerprinted, immutable-cacheable URLs),
`{t:key}` (`IWebTextLocalizer`), `{weblogic_scripts}`; `{foreach}` gains `{empty}`; `{item}` /
`{alias}` write the current item.
- v1 theme syntax keeps working: `{{key}}` / `{{page.x}}` / `{{item.x}}` tokens (now encoded for
their context), `{if:accessgroup:…}`, `|uppercase` / `|lowercase`, `a→b` replacements; unknown
filters still pass the value through.
- Layouts nest, sections defined closer to the page win, partials take parameters, and recursion /
depth (32) are guarded. `WebResult.Page(...)` and `WebResult.Fragment(...)` (no layout,
`X-WebLogic-Fragment`).
- `{renderhead}` from `WebPageMeta`: title, description, robots, keywords, canonical, hreflang
alternates, Open Graph (incl. article times, image alt), Twitter cards and safely serialized
JSON-LD.
- Theme templates load from the active theme with containment and link refusal; cached compiled
templates revalidate by timestamp, then source hash, at most every
`Theme.TemplateChangeCheckSeconds`.
- Themed error pages: `errors/{status}.html`, then `errors/error.html`, falling back to the
built-in page; API / JSON clients still get ProblemDetails.
- Theme sources: Local, **Git** (CL.GitHelper; clone + fast-forward into `{data}/themes/{id}`) and
**Storage** (CL.Storage; atomic directory download). `ThemeSyncService.RefreshAsync()` re-syncs on
demand; a failed update keeps serving the last good copy. `ThemeSyncedEvent`.
- Content: our own `WebMarkdownRenderer` (CommonMark + GFM subset: headings, emphasis,
strikethrough, code, fenced/indented code, quotes, nested lists, tables, links, images,
autolinks; raw HTML is written as text; URLs scheme-checked) and `WebHtmlSanitizer` (allowlist,
re-serialized from our own `HtmlTokenizer`, always balanced, `rel` on links), both covered by XSS
corpora.
- Template hooks for later milestones are DI contracts with safe defaults: `ITemplateCsrfProvider`,
`ITemplateWidgetRenderer`, `ITemplateScriptProvider`, `ITemplateAuthorizer`,
`ITemplateGlobalsProvider`, `ITemplateFilter` (`web.AddTemplateFilter<T>()`).
- Testing: `TemplateTestRenderer` / `InMemoryTemplateLoader` render templates in unit tests
without a host; golden snapshots run on both engines.
- Security pipeline: ordered `IWebSecurityStage`s in two phases (pre-routing and endpoint),
registered through DI, validated at startup (unique names, required order and phase) and
switchable by name (`Security.DisabledStages`, logged). Rejections are audited and published as
`WebSecurityRejectedEvent` (no client addresses).
- Stages: forwarded headers (off by default, explicit proxies / CIDRs only, hop limit, safe
`X-Forwarded-Prefix`), host filter and canonical-host redirect, HTTPS redirect to the
`PublicBaseUrl` host (never the Host header), tenant (`IWebTenantResolver`), IP and country rules
(CL.NetUtils GeoIP, cached), DNSBL reputation (CL.NetUtils; blocking with a time budget or
background), global and per-route token-bucket rate limiting (`429` with `Retry-After` and
`RateLimit-*`; unknown policies fail closed), authentication, request limits (`413`, form field
limits), authorization and CSRF.
- Response hardening applied when the response starts: nonce-based CSP with `'strict-dynamic'`
(configurable sources, report-only), HSTS on HTTPS, nosniff, frame protection, Referrer-,
Permissions-, COOP and CORP policies, `no-store` for signed-in users and CSRF-bearing responses.
Handlers cannot weaken them except through `request.Security.OverrideHeader` (logged).
- Sessions: one lazy session per request (nothing stored, no cookie until written), `__Host-`
cookie over HTTPS, token stored only as a SHA-256 hash, idle and absolute timeouts, optional IP
binding, rotation on sign-in / sign-out / privilege change, sign out everywhere, a sweeper, and
the `IWebSessionStore` contract (`web.UseSessionStore<T>()`).
- Identity: `WebIdentity` (scheme, ambient flag, groups, permissions, scopes, tenant, amr),
`IWebAuthenticationHandler` chain (session cookie built in; API keys / bearer / trusted header
come with the auth milestone), `WebLogic` ASP.NET scheme so `RequireAuthorization()` works on
minimal APIs.
- Authorization: routes are secure by default; groups, permissions (wildcards, resolved lazily
through `IWebPermissionResolver`), named policies (`web.AddAuthorizationPolicy`), custom and
resource-based requirements, `IWebAuthorizationService` for handler code. Pages redirect to
`Security.LoginPath` with a safe `returnUrl`; APIs get `401` + `WWW-Authenticate`; unknown
policies deny.
- CSRF v2: session-bound synchronizer tokens (DataProtection-protected pre-session cookie before
sign-in), masked per render, enforced for every unsafe request to WebLogic routes including
anonymous ones and the fallback, plus `Sec-Fetch-Site` and `Origin`/`Referer` checks; tokens
rotate on sign-in / sign-out; `GET /_weblogic/csrf` for SPAs; `{csrf}` in templates.
- Audit: bounded background queue to an app-supplied `IWebAuditSink`; client IPs are HMAC hashed.
- `DevelopmentStoreGuard` refuses to start outside Development with in-memory security stores
unless `Security.AllowDevelopmentStores` is set.
- `request.Identity`, `request.Session`, `request.Security`; `WebUrl.SafeReturnUrl`.
- Testing: `ManualTimeProvider`; the test host allows development stores by default.
- Auth flows (`Auth/`): `request.Auth` with password sign-in, TOTP and recovery-code second
factor, magic links, required password change, sign-out (everywhere), refresh and password
change; `WebSignInStatus` results that never reveal whether an account exists.
- Users: `WebUser` + `IWebUserStore` with optional lockout, e-mail and external-login
capabilities; null default store, `web.UseUserStore<T>()`, `web.UseInMemoryAccounts()` for
development.
- Passwords: own PBKDF2-SHA256 hasher (`$wl$pbkdf2-sha256$v=1$i=…`, 600k iterations, optional
pepper, automatic rehash, pluggable legacy algorithms), NIST-style `WebPasswordPolicy`,
per-login exponential back-off and per-IP cap (`IWebLoginThrottle`), account lockout.
- Two-factor: enrollment with QR code via CL.TwoFactorAuth, secrets stored DataProtection-
protected, every time step accepted once, hashed single-use recovery codes
(`IWebRecoveryCodeStore`).
- Account lifecycle (`WebAccountService`): registration, e-mail verification, password reset,
e-mail change (confirmed at the new address, old address told), magic links. Tokens are
DataProtection-protected, purpose-bound, time-limited, bound to the security stamp and
single-use (`IWebSecurityTokenService`, `IWebOneTimeUseStore`).
- Security stamps: credential changes end other sessions (checked every
`Auth.SecurityStampCheckMinutes` through the new `IWebSessionValidator` hook); password resets
end all sessions at once.
- Notifications: background `WebNotificationDispatcher`; `MailSecurityNotifier` (CL.Mail
templates `weblogic.{kind}` with built-in fallback texts), development outbox writing `.eml`
files, null notifier by default.
- API authentication: API keys `wl_{prefix}_{secret}` (SHA-256 at rest, constant-time check,
scopes, groups, tenant, expiry, revocation, allowed networks, per-key rate policy) via
`X-Api-Key` / `Authorization`; app-validated bearer tokens (`web.AddBearerTokens<T>()`);
trusted proxy headers (trusted peer + shared secret only); ASP.NET schemes such as Negotiate
(`web.AddAspNetAuthenticationScheme`).
- External login: `IWebExternalLoginProvider` + `WebExternalLoginFlow` (protected state/nonce
cookie, safe return URL, account linking).
- Tenancy: `web.UseHostTenants(map)`; users, sessions and API keys of another tenant are
rejected.
- Events: `UserSignedInEvent`, `UserSignedOutEvent`, `SignInFailedEvent`, `UserLockedOutEvent`,
`UserRegisteredEvent`, `PasswordChangedEvent`, `EmailConfirmedEvent`, `TwoFactorChangedEvent`,
`RecoveryCodeRedeemedEvent`, `ApiKeyChangedEvent`.
- `DevelopmentStoreGuard` now also covers the account stores and the outbox.
- Client runtime v2 (`Client/src`, vanilla ES2022, no npm dependencies, CSP-safe): config from
`#wl-config`, event bus, `http` (same-origin-only CSRF header with lazy token fetch and one
retry after rotation, keyed cancellation, timeouts, ProblemDetails → `HttpError`), safe `dom`
helpers, page `modules` (mount/unmount), allowlisted `commands`, ajax `forms` (schema-driven
client validation, double-submit guard, XHR upload progress, form results), `widgets`
(`data-wl-action` / `data-wl-refresh`), `realtime` (SSE or WebSocket, back-off with jitter,
last-event-id resume), partial `navigation` for `[data-wl-shell]` pages (head/title swap,
history, focus and announcements, full-load fallback), and a pluggable `ui` with `plain` and
`bootstrap5` adapters. `Client/weblogic.d.ts` ships in the package.
- Our own bundler (`tools/CL.WebLogic.ClientBundler`): exact JS lexer (strings, templates,
regex vs division, comments), strict module subset, dependency-ordered linking into ESM and
classic bundles, comment stripping, Brotli/gzip, content hashes and SRI. Runs incrementally
from MSBuild; output is embedded in `CL.WebLogic.dll`.
- Serving: `/_weblogic/client/{name}.{hash}.{ext}` (immutable, precompressed, ETag/304);
`{weblogic_scripts}` writes the config block, stylesheet and scripts with SRI and the CSP
nonce (no CSRF token, so pages stay cacheable). New `Client` config section (adapter, format,
navigation, stylesheet, test page).
- Head meta written by `WebPageHeadRenderer` is marked `data-wl-head` so navigation can swap it.
- Tests: bundler unit tests, `node --test` suites for the DOM-free modules, and a browser suite
at `/_weblogic/tests` (Development) that the integration tests run in headless Chrome. CI gains
a client job; the release pre-flight checks the client is embedded and the bundler is not a
dependency.
- Forms v2 (`Forms/`): definitions from DataAnnotations (`Required`, `StringLength`, `Range`,
`RegularExpression` with a 1 s timeout, `EmailAddress`, `Url`, `Phone`, `Compare`, `DataType`,
any other `ValidationAttribute`, `IValidatableObject`) plus `[WebForm]`, `[WebField]`,
`[WebOptions]`, `[WebShowWhen]`, `[WebFile]`, `[WebRepeater]`, `[WebStep]` and `[WebIgnore]`;
fluent `IWebFormConfigurator<T>`; definitions cached per type in a `ConditionalWeakTable`.
- `request.Forms.BindAsync<T>()`: binds only defined fields (no over-posting), invariant browser
formats, real checkboxes, multi-value fields, indexed repeaters (`Lines[0].Product`), hidden
conditional fields skipped, rich text sanitized, fixed and provider choices checked, async
cross-field `IWebFormValidator<T>`s. Messages go through `IWebTextLocalizer` (`forms.*` keys).
- `FormResult`: the client's JSON result (`ok`, `formId`, `errors`, `summary`, `message`,
`redirect`, `commands`, `data`, `step`; 422 for errors); plain posts get a 303 on success and the
fallback page re-rendered with values and errors.
- Rendering: `IWebFormRenderer` with `bootstrap5` (default) and `plain` renderers, native
validation attributes, ARIA (`aria-invalid`, `aria-describedby`, alert summary), the client
schema block, CSRF, honeypot and timestamp fields; theme overrides per field type
(`forms/fields/{type}.html`). `{form:id action=… class=… submit=… renderer=…}` template
directive (both engines); `request.Forms.Prefill(model)` / `RenderAsync<T>()`.
- Anti-bot without third parties: honeypot plus a protected render timestamp (minimum and maximum
age); `FormBotRejectedEvent`, `FormSubmittedEvent`.
- Multi-step forms: progress in a DataProtection-protected token (stateless, cluster-safe);
earlier steps can't be altered by later posts; Back button; works without script.
- Uploads: count and size limits, extension allowlist, content sniffing that must match the
extension, our own `ImageHeaderReader` (PNG, JPEG, GIF, WebP, BMP dimensions), cleaned display
names; `IWebUploadSink` with a CL.Storage-backed default that stores under random names.
- Endpoints, always registered: remote field validation
(`POST /_weblogic/forms/{form}/validate/{field}`, CSRF) and options/search
(`GET /_weblogic/forms/options/{provider}`, anonymous or policy per provider), rate limited
by `Forms.EndpointRatePolicy`. `WebFormHttpLookupProvider` takes an absolute https URL template
fixed in code and calls only hosts in `Forms.AllowedLookupHosts` (the old Host-header SSRF is
---
Release notes truncated to fit NuGet's 35 000 character limit. The complete changelog ships inside this package as CHANGELOG.md.