Skip to main content

Custom Sign In and Session Security

The partner route opens on the blue AccessOps · Partner sign-in window of the walkthrough: "Sign in to AccessOps", "Partner route · documents and approval requests", User name, Password, Sign in, a status banner and the SERVER LoginThrottle · failures N of 5 line at the bottom. Every refusal - wrong password, unknown name, disabled account, unapproved tenant, lockout - shows the same red sentence "That user name and password combination is not valid." A good password shows "Signing in…", the tab reloads, the session ID rotates on that page load and ConsolePage replaces the login view: AccessOps · Documents with the side strip Documents · Approvals · Sign out, the Documents heading with Export and the grid ID · Title · Tenant · Class (17 Supplier contract — Fabrikam, 21 Approval request AR-204, 24 Partner onboarding checklist, all Tenant A). Sign out revokes the account's login grants, expires the host cookie over HTTPS and ends the session; the tab comes back with "You have signed out. Sign in to continue." A tab that was open elsewhere still shows Export, and its next click answers "Your sign-in has ended. Sign in again to continue." with zero repository reads.

Solution AccessOps.slnx = AccessOps/ (Wisej.NET web app, net10.0, Wisej-4 4.1.7, Microsoft.Data.Sqlite 10.0.14) + AccessOps.Tests/ (xUnit, Microsoft.AspNetCore.TestHost 10.0.14) + PolicyHarness/ (plain .NET console). Started from a copy of Module 1; Module 1's training verifier, admin console and diagnostics view are replaced by the partner sign-in. Data (SQLite database, audit-pseudonym key) lives in AccessOpsData/ beside the project, outside the web root, git-ignored. Module 5 starts from this folder.

Run it​

cd "Security and Auth Patterns Course\Module 4"
dotnet build AccessOps.slnx
dotnet run --project AccessOps --urls https://localhost:6404 # HTTPS dev certificate
dotnet test AccessOps.slnx
dotnet run --project PolicyHarness

https://localhost:6404 needs the ASP.NET Core development certificate to be trusted by your browser (dotnet dev-certs https --trust, your decision). To check the sample in a browser that does not trust it (the course machine's Chromium browser did not), run a second, plain-HTTP listener in Development only:

$env:AccessOps__LocalHttpVerification = "true"
$env:AccessOps__AllowedOrigins__1 = "http://localhost:6444"
dotnet run --project AccessOps --urls "https://localhost:6404;http://localhost:6444"

AccessOps:LocalHttpVerification lets only the antiforgery cookie follow the request scheme (ASP.NET Core refuses to issue a Secure-only antiforgery cookie on plain HTTP); the host ticket stays Secure (Chrome accepts Secure cookies on http://localhost). The app refuses to start with it outside Development. Fresh fixtures: stop the app and delete AccessOpsData/.

Demo accounts (invented, local only, Debug builds only)​

User namePasswordAccountResult
partner.ana@fabrikam.testFabrikam-demo-17Ana Ruiz, tenant A, Readersigns in
partner.old@fabrikam.testFabrikam-demo-olddisabled, tenant Ageneric failure
partner.kai@northwind.testNorthwind-demo-23tenant C (not approved)generic failure

Passwords are PBKDF2-HMAC-SHA256 (210,000 iterations, 16-byte salt) hashes derived when a Debug build seeds the database; a Release build seeds the accounts without any password hash, so no fixture secret ships.

What to try​

Use two tabs (each tab is its own Wisej.NET session; the "two browser profiles" also gives each its own cookies).

ActionExpected result
partner.ana@fabrikam.test + a wrong password"That user name and password combination is not valid." (red), LoginThrottle · failures 1 of 5, password box empty
partner.old@fabrikam.test + Fabrikam-demo-oldthe same sentence (audit: Disabled)
partner.kai@northwind.test + Northwind-demo-23the same sentence (audit: TenantNotApproved)
partner.ana… + five wrong passwords, then Fabrikam-demo-17"failures 5 of 5", then the same sentence: locked for 15 minutes (audit: LockedOut)
partner.ana… + Fabrikam-demo-17 (fresh name or after 15 min)"Signing in…", reload, AccessOps · Documents; log RefreshSessionId() · rotated=True · isWebSocket=False
Console: Export (row 17 selected)record-A-17.txt downloads, "Record 17 exported · Supplier contract — Fabrikam" (green)
Tab 1 and tab 2 signed in as Ana; tab 1: Sign outtab 1 reloads: "You have signed out. Sign in to continue.", SERVER "grant revoked · context cleared · Application.Exit()"
Tab 2 (stale, fixture F05): Export"Your sign-in has ended. Sign in again to continue." (red); log export denied reason=grant … repositoryReads=+0; no new grant
Tab 2: refresh (F5), Exportconsole stays (same session), Export denied again, 0 reads
Tab 2: fetch('/auth/status') in the console401 (the host cookie's grant check)
fetch('/auth/signout', {method:'POST'}) without a token (forged post, F11)400, nothing changes
WebSocket upgrade to /ws.wx with Origin: https://evil.example (F11)403 before Wisej.NET; own origin 101
/auth/continue?returnUrl=//evil.example (also /\evil.example, https://evil.example/x) (F11)redirect to /
dotnet test36 tests green (docs/test-output.txt)
dotnet run --project PolicyHarnessone allowed export, every deny at zero repository calls (docs/policy-harness-output.txt)

Where this sample differs from the lesson or video​

  • Window title: the mock reads "AccessOps — Partner sign-in"; the app uses the course's AccessOps · <Area> form ("AccessOps · Partner sign-in", "AccessOps · Documents").
  • No area strip: the Module 4 mock has no Departments · Employees · … strip; the console uses the mock's side strip (Documents · Approvals · Sign out). Approvals is an empty screen (not built in this lab).
  • "Checking…" button state: the mock greys the button while checking; the check runs in one click handler, so the app shows "Signing in…" only after it passed.
  • SERVER line: kept as the mock shows it. It counts failures for the typed name, unknown names included, so it never tells whether an account exists; a production login view would not show it.
  • Sign-out scope: the video says "Grant G-7f3a… revoked by tab 1 sign-out"; here each sign-in has its own grant and sign-out revokes every active grant of the account, which is what makes tab 2's stale export fail.
  • Grid selection: Wisej's Bootstrap-4 theme draws the selected row grey, not the mock's light blue.
  • Grant IDs are G- + 32 hex characters, shown as G-xxxx… (the mock's G-7f3a… is an example).
  • Host cookie hand-off: the lesson says the session "creates" the handle; here the page load creates it and the page's own script posts it to /auth/cookie with an antiforgery token (Application.Call, value passed as an argument).
  • Sign-out sequencing: await Application.CallAsync("AccessOpsAuth.signOut", "signout") (the POST to /auth/signout) and only then Application.Exit() - script sent in the same response as Exit() never runs.
  • ApplicationPrincipal: as the lesson says, this copy assigns Application.User without rotating; SignInHandoff rotates once on the load, before Establish (Module 1's copy rotated inside).
  • Verification listener: AccessOps:LocalHttpVerification is not part of the lesson; it exists only so the sample can be checked in a browser that does not trust the dev certificate.