FreshIdentity.Core 2.0.0-preview.0.4

This is a prerelease version of FreshIdentity.Core.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package FreshIdentity.Core --version 2.0.0-preview.0.4
                    
NuGet\Install-Package FreshIdentity.Core -Version 2.0.0-preview.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FreshIdentity.Core" Version="2.0.0-preview.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FreshIdentity.Core" Version="2.0.0-preview.0.4" />
                    
Directory.Packages.props
<PackageReference Include="FreshIdentity.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FreshIdentity.Core --version 2.0.0-preview.0.4
                    
#r "nuget: FreshIdentity.Core, 2.0.0-preview.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FreshIdentity.Core@2.0.0-preview.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FreshIdentity.Core&version=2.0.0-preview.0.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FreshIdentity.Core&version=2.0.0-preview.0.4&prerelease
                    
Install as a Cake Tool

FreshIdentity

Identity and Auth service for mere mortals. Batteries included. 😊

FreshIdentity takes care of

  1. Identity – People or services.
  2. Authentication – Who are you? Passwords, GitHub, and phishing-resistant passkeys.
  3. Authorization – What are you allowed to do?

out of the box.

The main usage scenario is to provide

  • single-sign-on (SSO), and
  • role-based authorization

for other apps and services.

How-to

Force a password change on next login

fresh property set <user> <email> mustChangePassword true

Configuration

The service listens on plain HTTP at http://localhost:5555. Terminate public TLS at a reverse proxy and forward traffic to port 5555. Keep machine-specific database paths and credentials in the ignored appsettings.Development.json or in IdentityConfig__... environment variables.

Browser authentication is organized into persisted authentication sites. A site contains one or more exact public origins, a login title, its enabled authentication methods, an optional cookie domain, its passkey relying-party ID, and GitHub bindings. Accounts, credentials, roles, and properties remain global; a site is a browser session boundary, not a tenant. Runtime site changes apply without restarting the service and survive restarts in db.json. IdentityConfig:Sites:Bootstrap is seed-only: once a store contains sites, changing bootstrap settings does not overwrite them.

Cookies are host-only by default. Configure a site's parent cookie domain only when its related origins intentionally share sessions and every sibling host is equally trusted: protected cookies are bearer values that a compromised sibling can replay even though it cannot decrypt them. Cookies and continuations are cryptographically bound to the site and cannot be replayed at an unrelated site.

Every browser request must resolve to one enabled exact site origin. Production origins require HTTPS; loopback HTTP is accepted only for development. DNS, TLS, and request routing remain operator responsibilities. FreshIdentity does not discover or configure proxies, containers, certificates, or routes. If a Production store has no site, the administrative API remains available for recovery while browser authentication returns 404.

Redirect roles use boundary-aware scopes. For example, https://app.example:redirect permits that origin and paths below it, but not https://app.example.evil.test. OAuth2 client callback URIs are matched exactly, including their query string.

Persistence and bootstrap access

Set IdentityConfig__DataBaseDir to a writable, persistent directory for every administrable deployment. Without it, FreshIdentity uses an ephemeral in-memory store and deliberately does not reveal its bootstrap API key.

When a new persistent store is created, FreshIdentity writes db.json, Data Protection keys under keys/, and a one-time bootstrap-apikey.txt into that directory. The key belongs to the initial global-admin account (*:*). Load it into a secret manager or protected environment variable, then delete the bootstrap file. Restarts reuse db.json and do not recreate the file; deleting the file does not revoke the credential hash stored in the database. Restrict the entire directory to the service account because it contains password and machine-credential hashes, identity data, authorization state, and cookie-protection keys. FreshIdentity applies owner-only modes on Unix; on Windows, configure a service-account-only directory ACL.

The database has an explicit schema version. Opening an older unversioned store automatically migrates API keys and OAuth client secrets to one-way SHA-256 hashes while preserving the existing credentials. Schema version 1 stores are upgraded when passkey collections are initialized, and schema version 2 stores are upgraded to version 3 when authentication sites and site-bound passkey metadata are added. Raw machine and enrollment secrets are never written to the versioned db.json; they are returned only at creation. Passkey public keys, credential IDs, attestation objects, and client data are not authentication secrets, but they are security- and privacy-sensitive account metadata and remain protected by the data-directory controls. Existing passkeys can be migrated only when exactly one passkey-enabled site can be resolved safely; startup fails rather than guessing between relying parties. The site-bound cookie protection purpose signs out existing browser sessions during this upgrade. The migration is forward-only, so back up the protected data directory before upgrading if binary rollback is required. Backups of a pre-migration database still contain live secrets and require the original strict protection.

New Data Protection keys are encrypted with current-user DPAPI on Windows, so the service must retain the same Windows identity. Existing plaintext key rings are not rewritten automatically: rotate them during a planned maintenance window by stopping the service and replacing keys/, which signs out every browser session. Browser sessions are bound to both the authentication-site version and account version, so site configuration changes and credential or account changes—including passkey enrollment, rename, or revocation—revoke previously issued sessions. The browser performing a successful self-service passkey change receives a replacement session. Routine WebAuthn signature-counter and backup-state updates deliberately preserve the account version, so signing in with a passkey does not sign out other browsers. Normal logout clears only the current browser; mutate the account (for example, change its password) when every issued session must be revoked.

FreshIdentity currently targets a single active service instance. OAuth codes, access tokens, GitHub correlation state, and short-lived single-use passkey ceremony state are process-local, and db.json is not a distributed store. Horizontal deployment requires a shared account/state implementation; sharing only the Data Protection key ring is insufficient. Until then, do not run multiple writers (and keep requests sticky if using a warm standby). A future shared key ring must use an explicit certificate or external key-encryption provider.

POST /api authenticates from Authorization: apikey <key>; any authorization value in the JSON body is ignored. The key must be enabled and unexpired, its account must be enabled, and the account must have the global-admin role. Keep API keys out of URLs, committed settings, and logs.

Administrative reads return explicit safe account, authentication-site, GitHub-binding, password, GitHub-identity, API-key, OAuth-client, passkey, and pending-enrollment summaries. They never include password hashes, machine-credential or enrollment hashes and secrets, passkey public-key/attestation blobs, or raw GitHub provider data. API keys have non-secret credential IDs so they can be listed, updated, enabled, disabled, and revoked without supplying the secret. Newly created API keys, OAuth client secrets, and passkey enrollment secrets are returned only by their creation call; capture them immediately. API responses are marked no-store.

Administrative mutations accept optional account or site ETags. A stale precondition is rejected with HTTP 409 and etag_mismatch. Operations that would remove the final active global-admin API access path are rejected with last_administrator; disabling or deleting the final usable browser site is rejected with last_authentication_site. The caller must explicitly request the corresponding lockout override. Use that override only during a planned decommission. Successful administrative mutations emit structured audit logs containing the actor account, non-secret API-key ID, operation, target, result, timestamp, and trace ID; secrets and property values are never audit fields.

New and changed passwords must contain 12–256 characters and cannot be blank or whitespace-only. They are protected with versioned PBKDF2-HMAC-SHA512 hashes. Existing legacy SHA-512 credentials remain usable, including passwords up to 8,192 characters (subject to the 16 KiB encoded form limit), and are upgraded automatically after a successful password login. New credentials cannot use those grandfathered lengths.

Password sign-in and password-change submissions are limited to 10 attempts per minute per client IP. Passkey option, registration, assertion, rename, and revocation requests share a 30-request-per-minute per-client-IP limit. Browser forms use antiforgery tokens, and accounts marked mustChangePassword receive only a short-lived restricted session until the change succeeds. OAuth authorization and token requests are each limited to 60 requests per minute per client IP; state is limited to 1,024 characters, and the in-memory authorization-code store allows at most 256 codes per client and 4,096 total. The access-token store allows 256 tokens per identity and 4,096 total. OAuth authorization-code failures return JSON OAuth errors with protocol-appropriate status codes and no-store caching. GitHub login endpoints allow 20 requests per minute per client IP, and upstream calls time out after 10 seconds. When deploying behind a proxy, list its IP address in the semicolon-separated IdentityConfig__KnownProxies setting (for example, 10.0.0.10). FreshIdentity accepts forwarded client/protocol headers only from loopback and those explicit proxies.

Forward-auth clients pass their API key in the X-FreshIdentity-ApiKey header; never place it in the query string. Browser authentication and continuation cookies are encrypted, authenticated, Secure, HttpOnly, and SameSite=Lax, using the persisted Data Protection keys.

Passkeys

Passkeys are a standard site authentication method. They use the .NET 10 WebAuthn verifier, discoverable credentials, and required local user verification (device PIN or biometrics). FreshIdentity accepts synced consumer passkeys, platform authenticators, password managers, and user-verifying security keys. It requests no authenticator attestation and does not claim AAL3 hardware-key assurance. Passkey private keys never reach FreshIdentity.

Development automatically creates a localhost site with password and passkey authentication enabled, so no passkey environment flag is required. A Production bootstrap site can be seeded with exact configuration:

IdentityConfig__Sites__Bootstrap__primary__Origins=https://login.example.org
IdentityConfig__Sites__Bootstrap__primary__Name=Primary
IdentityConfig__Sites__Bootstrap__primary__LoginTitle=Example login
IdentityConfig__Sites__Bootstrap__primary__PasskeyRelyingPartyId=login.example.org

The relying-party ID is a DNS domain without a scheme, port, or path. An origin includes the public scheme, host, and any non-default port, but no path. Use the most specific stable login hostname: a parent RP ID such as example.org intentionally makes the credential available to eligible subdomains and expands the trust boundary. An RP ID becomes immutable while passkeys or a pending enrollment reference its site. Multiple related origins may share a site and RP ID; unrelated RP IDs require separate passkeys even when the account is shared.

The legacy IdentityConfig__Passkeys__ServerDomain and IdentityConfig__Passkeys__AllowedOrigins pair remains a bootstrap adapter for a store without sites. IdentityConfig__Passkeys__Enabled=false is an emergency global kill switch; Enabled=true is unnecessary. A trusted TLS-terminating proxy must preserve the public host and Origin and forward the trusted protocol as described above.

An already signed-in person can add, rename, and revoke passkeys on the account page. To provision a passwordless account, an administrator creates the account and a short-lived, single-use enrollment:

dotnet run --project src/fresh/fresh.csproj -- account create --label "Example person"
dotnet run --project src/fresh/fresh.csproj -- passkey enrollment create <account-id> --site <site-id> --minutes 60

The second command prints the enrollment secret exactly once. Send it through an appropriately protected channel; it is a bearer credential until used or expired. The database stores only its SHA-256 hash. The user opens /auth/passkey/enroll, pastes the code, names the passkey, and completes the browser prompt. Successful registration consumes the enrollment atomically and signs in the new account. Creating a replacement enrollment invalidates the previous one. Public anonymous account signup is deliberately not provided.

Operators can inspect safe metadata and recover an account whose authenticators were lost:

fresh passkey list <account-id>
fresh passkey rename <passkey-id> "Work laptop"
fresh passkey revoke <passkey-id>
fresh passkey enrollment create <account-id> --site <site-id> --minutes 60
fresh passkey enrollment revoke <enrollment-id>

A passkey-only user should register multiple authenticators or use a synced passkey provider. Administrative revocation plus a new enrollment is the recovery path. Enrollment is capped at 24 hours; an account can hold at most 10 passkeys; names are limited to 200 characters. Ceremony state is server-side, single-use, and retained for five minutes. Passkey authentication is independent of a password credential's mustChangePassword flag.

GitHub OAuth

GitHub client registrations remain operator/startup configuration so client secrets can come from environment or secret providers rather than db.json. Sites bind to configured registration IDs and exact callback URIs at runtime. One GitHub OAuth App may serve up to ten exact callbacks, or sites may use separate registrations for independent ownership, branding, rotation, and blast-radius reduction.

IdentityConfig__Github__Registrations__main__DisplayName=GitHub
IdentityConfig__Github__Registrations__main__ClientId=<github-client-id>
IdentityConfig__Github__Registrations__main__ClientSecret=<github-client-secret>
IdentityConfig__Github__Registrations__main__CallbackUris=https://login.example.org/auth/github/callback

CallbackUris seeds bindings only while the database has no persisted sites. After bootstrap, add or remove bindings with fresh site github; update the corresponding callback list at GitHub separately. Callbacks must be exact site URLs ending in /auth/github/callback; wildcards are rejected. FreshIdentity uses direct site callbacks, short-lived single-use server-side state, a protected site correlation cookie, and PKCE S256. OAuth codes, state, PKCE verifiers, access tokens, and client secrets are never logged or persisted.

The legacy singular IdentityConfig__Github__ClientId, ClientSecret, and CallbackUri settings create a registration named default during migration.

appsettings.json

{
  "IdentityConfig": {
    "DataBaseDir": "./db",
    "Sites": {
      "Bootstrap": {
        "primary": {
          "Name": "Primary",
          "LoginTitle": "Example login",
          "Origins": "https://login.example.org",
          "CookieDomain": ".example.org",
          "PasswordEnabled": true,
          "PasskeysEnabled": true,
          "PasskeyRelyingPartyId": "login.example.org"
        }
      }
    },
    "Github": {
      "Registrations": {
        "main": {
          "DisplayName": "GitHub",
          "ClientId": "<github-client-id>",
          "ClientSecret": "<github-client-secret>",
          "CallbackUris": "https://login.example.org/auth/github/callback"
        }
      }
    },
    "SignalCliRestApi": {
      "Url": "http://x.x.x.x:port",
      "Number": "+12345xxxx"
    }
  }
}

docker-compose.yml

services:

  login:
    # ...
    environment:
      - IdentityConfig__DataBaseDir=/data
      - IdentityConfig__Sites__Bootstrap__primary__Name=Primary
      - IdentityConfig__Sites__Bootstrap__primary__Origins=https://login.example.org
      - IdentityConfig__Sites__Bootstrap__primary__CookieDomain=.example.org
      - IdentityConfig__Sites__Bootstrap__primary__PasskeyRelyingPartyId=login.example.org
      - IdentityConfig__Github__Registrations__main__DisplayName=GitHub
      - IdentityConfig__Github__Registrations__main__ClientId=<github-client-id>
      - IdentityConfig__Github__Registrations__main__ClientSecret=<github-client-secret>
      - IdentityConfig__Github__Registrations__main__CallbackUris=https://login.example.org/auth/github/callback
    volumes:
      - ./data:/data
    # ...

Quick start

Local

dotnet tool restore
dotnet paket restore
$env:IdentityConfig__DataBaseDir = Join-Path $PWD "data"
dotnet run --project src/FreshIdentity.Login/FreshIdentity.Login.csproj

Open http://localhost:5555 after the service starts.

On the first start, load the bootstrap key without placing its value in shell history, then delete the one-time file:

$env:FRESHIDENTITY_API_KEY = (Get-Content .\data\bootstrap-apikey.txt -Raw).Trim()
Remove-Item .\data\bootstrap-apikey.txt

fresh CLI

Run the CLI from source with dotnet run --project src/fresh/fresh.csproj --, or use the executable produced under publish/fresh/ by the build scripts. An explicit connection is always an endpoint/key pair; the CLI never combines a cached key with an overridden endpoint or an environment key with a cached endpoint.

For the local service, pair the bootstrap key above with its endpoint:

$env:FRESHIDENTITY_CONNECTION_URI = "http://localhost:5555/api"
dotnet run --project src/fresh/fresh.csproj -- info

Runtime sites are managed without restarting the service:

fresh site list
fresh site create --name "Customer A" --origin https://login.customer-a.example --rp-id login.customer-a.example
fresh site origin add <site-id> https://alternate.customer-a.example
fresh site github bind <site-id> main https://login.customer-a.example/auth/github/callback
fresh site disable <site-id>

Site mutations use ETags automatically through the CLI. Disabling or deleting the final usable browser site requires --allow-lockout; sites with passkeys or pending enrollment cannot be deleted. Removing an origin first requires removing any GitHub binding that uses it. GitHub registration IDs, labels, and enabled status available to runtime bindings are reported by fresh info.

Only loopback development endpoints may use HTTP. Every non-loopback endpoint must use HTTPS. --connectionuri <uri> is an alternative to FRESHIDENTITY_CONNECTION_URI, but it must still be paired with FRESHIDENTITY_API_KEY. If both endpoint sources are present, they must identify the same URI. Supplying only one member of an explicit pair or conflicting endpoints is a configuration error and no request is sent.

To cache the explicit pair locally and make it the default:

dotnet run --project src/fresh/fresh.csproj -- credentials add local
dotnet run --project src/fresh/fresh.csproj -- credentials default local
$env:FRESHIDENTITY_CONNECTION_URI = $null
$env:FRESHIDENTITY_API_KEY = $null
dotnet run --project src/fresh/fresh.csproj -- info

Credential display commands report only whether a key exists; they never print cached key values. Newly created API keys, OAuth client secrets, and passkey enrollment secrets are necessarily printed once by their creation commands and should be captured securely. Passwords and API keys are read from their documented environment variables or secure input rather than command-line arguments.

Commands are completely validated before prompts or API calls. Account and OAuth listings are bounded to 50 items by default and 200 items per server request; use --all to retrieve every page. Account search supports labels, identifiers, usernames, GitHub metadata, OAuth client IDs, passkey names and IDs, status, and credential-type filters. Administrator-created and reset passwords require a change on first login by default; pass --must-change false only when that is intentional.

Exit code 0 means success, 1 means an operational, HTTP, or logical API failure, 2 means invalid arguments or connection configuration, and 130 means cancellation. Logical failures retain their JSON response on stdout while diagnostics go to stderr. Use --diagnostics only when a stack trace is needed for an unexpected failure.

fresh property set -f <id> <scope> <key> <file> parses the file as one JSON value; without -f, the supplied value is stored as a string. Run fresh --help for the complete command reference.

Docker

docker build -t freshidentity .
docker volume create freshidentity-data
docker run --rm --name freshidentity -p 127.0.0.1:5555:5555 `
  -e IdentityConfig__DataBaseDir=/data `
  -v freshidentity-data:/data freshidentity

The image runs in Production by default. Every Production request must be HTTPS after trusted forwarded headers are processed. The TLS-terminating proxy described above must forward X-Forwarded-Proto: https from an address listed in IdentityConfig__KnownProxies; direct HTTP requests and requests from misconfigured proxies are rejected with HTTP 400 before authentication or antiforgery processing. For a direct, loopback-only UI smoke test, add -e ASPNETCORE_ENVIRONMENT=Development to the command. Never use that override for a deployed container.

The container exposes HTTP on port 5555 for the trusted proxy. In a second PowerShell session, capture and remove the bootstrap file after the first start:

$env:FRESHIDENTITY_API_KEY = (docker exec freshidentity cat /data/bootstrap-apikey.txt).Trim()
docker exec freshidentity rm /data/bootstrap-apikey.txt

The named volume inherits the image's service-user ownership. If you use a host bind mount instead, make that directory writable only by the container's APP_UID.

.NET

Install the current prerelease of FreshIdentity.Core:

dotnet add package FreshIdentity.Core --prerelease
var apiKey = Environment.GetEnvironmentVariable("FRESHIDENTITY_API_KEY")
    ?? throw new InvalidOperationException("Missing FreshIdentity API key.");

using var client =
    new IdentityServiceClient(
        endpoint: "https://login.example.org/api",
        apiKey  : apiKey
    );

Create Account

Creates a labelled, enabled account. It has no sign-in or machine credentials until they are added.

var x = await client.CreateAccountAsync(label: "Example service");
Response
{
  "account": {
    "id": "1e9b90d7a4ffc80886d5f8528f2ef72a",
    "label": "Example service",
    "displayLabel": "Example service",
    "isEnabled": true,
    "created": "2026-08-20T13:09:58.5219337+00:00",
    "updated": "2026-08-20T13:09:58.5219337+00:00",
    "etag": "ab030dfa56f147da7ad8608b9573892410426c48",
    "roles": [],
    "properties": {},
    "passwordCredentials": [],
    "githubIdentities": [],
    "apiKeys": [],
    "oAuth2Clients": [],
    "passkeys": []
  }
}

Manage Accounts

// get an account by id
var x = await client.GetAccountAsync(id: "1e9b90d7a4ffc80886d5f8528f2ef72a");

// find the latest 3 enabled API-key accounts
var xs = await client.ListAccountsAsync(
    take: 3,
    orderBy: "created",
    reverse: true,
    query: "service",
    isEnabled: true,
    credential: AccountCredentialKind.ApiKey
    );

// disable with optimistic concurrency
var account = x.Account;
var disabled = await client.UpdateAccountAsync(new(
    account.Id,
    IsEnabled: false,
    ExpectedEtag: account.Etag
    ));

// delete using the latest ETag
var reply = await client.DeleteAccountAsync(
    account.Id,
    expectedEtag: disabled.Account.Etag
    );
Console.WriteLine(
    reply.Deleted
    ? "account has been deleted"
    : "account does not exist"
    );

Manage machine credentials

Creation responses are the only place raw secrets appear. Persist the secret before discarding the response; subsequent reads return only safe metadata and the non-secret credential ID.

var account = (await client.GetAccountAsync("example-service")).Account
    ?? throw new InvalidOperationException("Account not found.");
var createdKey = await client.CreateApiKeyAsync(
    account.Id,
    description: "deployment automation",
    validUntil: DateTimeOffset.UtcNow.AddDays(90),
    expectedEtag: account.Etag
    );
StoreInSecretManager(createdKey.Secret);

await client.DeleteApiKeyAsync(
    createdKey.ApiKey.Id,
    expectedEtag: createdKey.AccountEtag
    );

Provision and manage passkeys

Passkeys themselves are created only through a browser WebAuthn ceremony. The administrative client creates the one-time enrollment credential and can list, rename, or revoke the resulting safe credential identifiers:

var account = (await client.GetAccountAsync("example-person")).Account
    ?? throw new InvalidOperationException("Account not found.");
var sites = await client.ListAuthenticationSitesAsync();
var site = sites.Items.Single(x => x.Name == "Primary");
var enrollment = await client.CreatePasskeyEnrollmentAsync(
    account.Id,
    DateTimeOffset.UtcNow.AddHours(1),
    account.Etag,
    site.Id
    );
DeliverEnrollmentCodeSecurely(enrollment.Secret);

var passkeys = await client.ListPasskeysAsync(account.Id);
await client.DeletePasskeyAsync(
    passkeys.Items.Single().Id,
    expectedEtag: passkeys.AccountEtag
    );

Run fresh --help for the complete account, API-key, OAuth2, password, passkey, role, property, concurrency, and lockout-override command reference.

Contributing

See CONTRIBUTING.md for build, test, coverage, CI, coding, and Aardvark.Build release-note guidance.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
2.0.0-preview.0.5 55 8/22/2026
2.0.0-preview.0.4 36 8/22/2026
2.0.0-preview.0.3 38 8/20/2026
2.0.0-preview.0.2 62 7/11/2026
2.0.0-preview.0.1 203 8/11/2025
1.0.19 475 4/4/2023
1.0.18 394 4/4/2023
1.0.17 365 4/4/2023
1.0.16 405 4/3/2023
1.0.12 404 3/17/2023
1.0.11 578 11/4/2022
1.0.10 561 11/3/2022
1.0.9 648 10/14/2022
1.0.8 601 8/31/2022
1.0.7 612 8/24/2022
1.0.6 603 8/24/2022
1.0.5 600 8/23/2022
1.0.4 606 8/22/2022
1.0.3 597 8/22/2022
1.0.2 633 8/22/2022
Loading failed

- Add persisted runtime authentication sites with exact public origins, site-scoped sessions, optional related-domain cookie sharing, per-site password/passkey/GitHub availability, ETags, lockout protection, and structured audits across API protocol 4, the .NET client, and `fresh site` commands.
- Make localhost password and passkey authentication work by default in Development; add safe Production bootstrap/admin-only recovery behavior and schema version 3 migration that binds existing passkeys, enrollments, cookies, OAuth continuations, and browser providers to explicit sites.
- Support multiple operator-configured GitHub clients and runtime site bindings, including shared or separate registrations, up to ten exact callbacks per registration, direct site callbacks, single-use server-side state, protected correlation cookies, PKCE S256, runtime rechecks, and secret/token redaction.
- Apply site resolution consistently to password login/change/logout, passkeys, GitHub, forward-auth, FreshIdentity OAuth authorization, account pages, continuations, and unknown-host rejection without introducing infrastructure-specific routing or discovery.
- Add safe site/RP metadata to passkey projections and enrollment selection, immutable in-use RP IDs, cross-site credential rejection, atomic runtime persistence, failed-write rollback, restart recovery, and final-site protections.
- Align the responsive login, enrollment, and account UI with site titles and enabled methods; render multiple GitHub choices accessibly and include the site context during passwordless enrollment.
- Split site administration and startup authentication configuration from account administration and process bootstrap, centralize exact origin/RP/cookie/callback validation and bounded single-use state, and remove global browser configuration and the obsolete static GitHub template.
- Expand deterministic multi-site, multi-registration, PKCE, cookie-isolation/sharing, migration, concurrency, CLI/client, and browser virtual-authenticator coverage to 463 tests; visually dogfood desktop/mobile enrollment, credential management, logout, and passkey sign-in.
- Include the repository README in the NuGet package and enforce its package metadata/content in the trusted-publishing workflow.
- Add phishing-resistant .NET 10 passkey sign-in, conditional browser UI, signed-in credential management, and administrator-provisioned passwordless account enrollment with required user verification and exact RP/origin configuration.
- Add safe passkey/enrollment administration across the API, SDK, and `fresh` CLI; persist only public WebAuthn material and hashed single-use enrollment secrets while keeping assertion metadata updates atomic without revoking unrelated sessions.
- Expand migration, concurrency, rejection-path, UI/provider, client, CLI, coverage, mutation, and virtual-authenticator dogfood validation; centralize antiforgery handling and clean up the account/login presentation.
Breaking changes: browser authentication now requires a matching persisted authentication site in Production; API protocol 4 adds site contracts and site/RP passkey metadata; schema version 3 and site-bound cookie protection are forward-only and sign out existing browser sessions; ambiguous legacy passkeys require one explicit passkey site rather than an inferred RP; unrelated RP IDs require separate passkeys; the Core `Config` browser/GitHub shape and administrative site/enrollment contracts changed; GitHub login initiation is registration-addressed at `/auth/github/{registrationId}` and uses exact per-site callbacks; legacy global domain, title, singular GitHub, and passkey RP/origin settings are seed-only adapters; and Development enables localhost passkeys unless the global kill switch is explicitly false.