Argon2id.PasswordHasher.AspNetCore
0.4.0-preview.2
See the version list below for details.
dotnet add package Argon2id.PasswordHasher.AspNetCore --version 0.4.0-preview.2
NuGet\Install-Package Argon2id.PasswordHasher.AspNetCore -Version 0.4.0-preview.2
<PackageReference Include="Argon2id.PasswordHasher.AspNetCore" Version="0.4.0-preview.2" />
<PackageVersion Include="Argon2id.PasswordHasher.AspNetCore" Version="0.4.0-preview.2" />
<PackageReference Include="Argon2id.PasswordHasher.AspNetCore" />
paket add Argon2id.PasswordHasher.AspNetCore --version 0.4.0-preview.2
#r "nuget: Argon2id.PasswordHasher.AspNetCore, 0.4.0-preview.2"
#:package Argon2id.PasswordHasher.AspNetCore@0.4.0-preview.2
#addin nuget:?package=Argon2id.PasswordHasher.AspNetCore&version=0.4.0-preview.2&prerelease
#tool nuget:?package=Argon2id.PasswordHasher.AspNetCore&version=0.4.0-preview.2&prerelease
<div align="center">
๐ Argon2id.PasswordHasher
The opinionated, secure-by-default Argon2id password hasher for .NET 8, 9, and 10.
Memory-hard password hashing that's hard to get wrong.
</div>
Argon2id.PasswordHasher wraps a vetted Argon2id implementation in a tiny, hard-to-misuse API with strong defaults out of the box. You get memory-hard resistance to GPU/ASIC cracking (RFC 9106 / OWASP-aligned), self-describing hashes that carry their own parameters, constant-time verification, an optional keyed pepper with rotation, and a one-line ASP.NET Core Identity integration โ without having to become a cryptographer first.
using Argon2id.PasswordHasher;
var hasher = new Argon2idPasswordHasher();
string stored = hasher.HashPassword("correct horse battery staple");
// $argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
bool ok = hasher.VerifyPassword("correct horse battery staple", stored); // true
Try the demo
There are two runnable samples, sharing the same UX so you can pick whichever fits what you want to show:
| Sample | Where it runs | When to use it |
|---|---|---|
| Live WASM demo | In your browser, no install | Quickest way to try the library. Hashing runs on your CPU via WebAssembly. Auto-deployed to GitHub Pages on every push. |
| API documentation | DocFx, browser | Every public type + member, generated from XML doc comments. Co-published with the live demo. |
samples/Argon2id.PasswordHasher.Demo |
Blazor Server (local) | Shows production-shape integration: DI, antiforgery, rate limiting, HSTS, CSP, constant-time login, memory-cost DoS gate. |
samples/Argon2id.PasswordHasher.WasmDemo |
Blazor WebAssembly (local) | Same UX as the live demo, but running against the in-tree library. Edit and refresh. |
Run either locally:
git clone https://github.com/systemslibrarian/argon2id-passwordhasher.git
cd argon2id-passwordhasher
# Server flavor (production-shape, hardened):
dotnet run --project samples/Argon2id.PasswordHasher.Demo
# WASM flavor (same UX as the live demo):
dotnet run --project samples/Argon2id.PasswordHasher.WasmDemo
Both samples use a ProjectReference to the in-tree library, so they always
exercise the version you're working on. They are demos, not starter
templates โ users live in memory and hash internals are shown on screen for
educational clarity (see each sample's own README).
Documentation
Documentation is layered so you only read the depth you need:
| Audience / question | Doc |
|---|---|
| "How do I use it?" | This README, plus API reference |
| "How do I migrate from Identity's default PasswordHasher?" | MIGRATION.md |
| "How should I run this in production?" | OPERATIONS.md โ capacity planning, monitoring, alerting, failure modes |
| "What about FIPS / SOC 2 / vendor questionnaires?" | COMPLIANCE.md |
| "What is the security threat model?" | THREAT-MODEL.md |
| "How do I store the pepper in Azure Key Vault / AWS / Vault?" | docs/pepper-key-management.md |
| "What's the support and lifecycle policy?" | SUPPORT.md |
| "How do I tune the work factor?" | docs/parameter-tuning.md |
| "What does the library deliberately NOT do?" | KNOWN-GAPS.md |
| "How do I report a vulnerability?" | SECURITY.md |
| "How do I contribute?" | CONTRIBUTING.md |
| "What changed in this version?" | CHANGELOG.md |
Table of contents
- Why this library
- Packages
- Install
- Quick start
- The hash format
- Configuration
- Upgrading work factor (rehash on login)
- Avoiding
stringpasswords (span overloads) - Pepper (secret key) with rotation
- ASP.NET Core Identity
- Trimming & Native AOT
- Security posture
- API reference
- FAQ
- Building, testing & benchmarks
- Versioning & status
- Contributing & security
- License & acknowledgements
Why this library
| ๐ก๏ธ Secure by default | The no-arg constructor gives you 64 MiB / t=3 / p=1 โ comfortably above the OWASP minimum. No footguns to configure. |
| ๐ฆ Self-describing hashes | Every hash is a standard PHC string containing its own parameters, so verification never breaks when you raise the work factor. |
| โป๏ธ Built-in upgrade path | NeedsRehash tells you when a stored hash is weaker than your current settings (or uses an old pepper) so you can upgrade users transparently. |
| โฑ๏ธ Constant-time | Final comparison uses CryptographicOperations.FixedTimeEquals. |
| ๐ถ๏ธ Pepper with rotation | Optional keyed secret kept outside your database, with first-class key rotation. |
| ๐งฉ ASP.NET Core ready | Drop-in IPasswordHasher<TUser> + a one-line DI extension. |
| ๐ Trim + AOT compatible | Marked IsTrimmable and IsAotCompatible so Native AOT consumers just work. |
| ๐งผ Tiny & honest | One runtime dependency. Every real limitation is documented in KNOWN-GAPS.md. |
Packages
| Package | Purpose | Depends on |
|---|---|---|
Argon2id.PasswordHasher |
Core hasher โ no web/framework dependency | Konscious.Security.Cryptography.Argon2 |
Argon2id.PasswordHasher.AspNetCore |
IPasswordHasher<TUser> adapter + DI extension |
the core package + Microsoft.Extensions.Identity.Core |
Both packages target net8.0, net9.0, and net10.0.
Install
# Core
dotnet add package Argon2id.PasswordHasher --prerelease
# Optional: ASP.NET Core Identity integration
dotnet add package Argon2id.PasswordHasher.AspNetCore --prerelease
Quick start
Registration โ hash the password and store the returned string:
var hasher = new Argon2idPasswordHasher();
user.PasswordHash = hasher.HashPassword(password);
await db.SaveChangesAsync();
Login โ verify against the stored string:
if (!hasher.VerifyPassword(password, user.PasswordHash))
return Unauthorized();
// Optionally strengthen the stored hash if your settings have grown:
if (hasher.NeedsRehash(user.PasswordHash))
{
user.PasswordHash = hasher.HashPassword(password);
await db.SaveChangesAsync();
}
VerifyPassword never throws on bad input: a null, empty, malformed, or non-Argon2id
stored value simply returns false.
Argon2idPasswordHasher is stateless and thread-safe. Create one and reuse it (e.g. register
it as a singleton) rather than constructing one per request.
The hash format
Hashes are emitted in the standard PHC string format used by libsodium and the Argon2 reference implementation:
$argon2id$v=19$m=65536,t=3,p=1$<base64 salt>$<base64 hash>
โโโ alg โโโโ ver โโโโ cost params โโโโโโ salt โโโโโโ hash โโโ
Because the parameters live inside the string, the hash is portable and self-verifying:
you can change your defaults whenever you like and old hashes keep validating with the parameters
they were created with. When a pepper is used, an extra keyid=<id> parameter is added so
verification can select the right secret.
Configuration
Pass an Argon2idOptions to tune the work factor. Invalid values (below the safe minimums) throw
ArgumentOutOfRangeException at construction โ fail fast, not silently weak.
var hasher = new Argon2idPasswordHasher(new Argon2idOptions
{
MemorySizeKib = 131072, // 128 MiB
Iterations = 4,
DegreeOfParallelism = 1,
});
| Option | Default | Minimum | Notes |
|---|---|---|---|
MemorySizeKib |
65536 (64 MiB) |
8192 |
Primary GPU/ASIC defense. Raise this first. |
Iterations |
3 |
1 |
Passes over memory (linear CPU cost). |
DegreeOfParallelism |
1 |
1 |
Lanes/threads per hash. Keep low on shared servers. |
SaltSizeBytes |
16 (128-bit) |
16 |
RFC 9106 recommendation. |
HashSizeBytes |
32 (256-bit) |
16 |
Output (tag) length. |
Why these defaults? They're a strong, general-purpose baseline for 2026 server hardware that
exceeds the current OWASP minimum (Argon2id, 19 MiB, t=2, p=1) while keeping p=1 so per-hash CPU
stays predictable under concurrent logins. They are not tuned to your machine โ measure and
adjust. See docs/parameter-tuning.md and the
benchmark project.
Argon2idOptions.Recommended exposes the defaults explicitly.
Upgrading work factor (rehash on login)
Security guidance gets stronger over time. When you raise your parameters, existing users upgrade themselves the next time they sign in โ no mass migration, no broken logins:
if (hasher.VerifyPassword(password, user.PasswordHash))
{
if (hasher.NeedsRehash(user.PasswordHash))
user.PasswordHash = hasher.HashPassword(password); // re-hashed with current settings
// sign the user in...
}
NeedsRehash returns true when the stored hash is unparseable, any stored parameter is below
your current configuration, or (with a pepper ring) the hash doesn't use your active pepper.
Avoiding string passwords (span overloads)
HashPassword / VerifyPassword also accept ReadOnlySpan<char> and ReadOnlySpan<byte>, so you
can hash a credential without ever materializing it as a string. The hasher zeroes every
password-derived buffer it owns.
ReadOnlySpan<char> pw = GetPasswordChars();
string hash = hasher.HashPassword(pw);
bool ok = hasher.VerifyPassword(pw, hash);
.NET cannot reliably wipe an immutable string from memory. Prefer the span overloads when the
password doesn't otherwise need to be a string. See KNOWN-GAPS.md ยง1.
Pepper (secret key) with rotation
A pepper is an application secret mixed into every hash and kept outside the database (in a
key vault, KMS, or environment variable). If your password table leaks but the pepper doesn't, the
stolen hashes can't be cracked offline. Peppers here are keyed and rotatable โ each hash records
which pepper produced it (via the PHC keyid), and the key bytes are never stored.
byte[] key = GetPepperFromVault(); // โฅ 16 bytes, kept secret
var ring = new PepperRing(new Pepper("2026-05", key));
var hasher = new Argon2idPasswordHasher(Argon2idOptions.Recommended, ring);
string hash = hasher.HashPassword(password);
// $argon2id$v=19$m=65536,t=3,p=1,keyid=<id>$<salt>$<hash>
Rotation โ promote a new active key and keep the old one as retired so existing hashes still
verify; NeedsRehash then upgrades them on the next login:
var rotated = new PepperRing(
active: new Pepper("2026-11", newKey),
retired: new Pepper("2026-05", oldKey));
The library never persists pepper keys โ that's your responsibility. Lose the active key and you lose the ability to verify hashes made with it. Back up and retire keys deliberately.
For end-to-end examples of loading peppers from Azure Key Vault, AWS Secrets Manager,
Google Cloud Secret Manager, HashiCorp Vault, or environment variables, plus the full
rotation playbook, see docs/pepper-key-management.md.
ASP.NET Core Identity
Install Argon2id.PasswordHasher.AspNetCore and register the hasher in one line:
builder.Services
.AddIdentityCore<IdentityUser>()
.Services
.AddArgon2idPasswordHasher<IdentityUser>(); // optional: pass Argon2idOptions and/or a PepperRing
This registers an IPasswordHasher<TUser> backed by Argon2id and shares a single core hasher as a
singleton. Verification maps cleanly onto Identity's contract:
| Result | When |
|---|---|
Success |
Password matches and the hash is up to date. |
SuccessRehashNeeded |
Password matches but the hash is weaker than current settings / uses an old pepper. Identity rehashes it automatically. |
Failed |
Password doesn't match, or the stored value is malformed. |
Trimming & Native AOT
Both packages are marked IsTrimmable=true and IsAotCompatible=true. No reflection, no dynamic
codegen, no System.Reflection.Emit โ the library uses only BCL crypto primitives plus the
Konscious managed Argon2 implementation. Native AOT consumers can publish trimmed binaries without
warnings.
How this library compares
Quick orientation for the common alternatives in the .NET ecosystem:
| Library | Algorithm | Memory-hard | Self-describing hash | First-class pepper | ASP.NET Identity adapter | TFMs |
|---|---|---|---|---|---|---|
Argon2id.PasswordHasher (this library) |
Argon2id (RFC 9106) | โ | โ (PHC) | โ + rotation | โ + migration adapter | net8 / net9 / net10 |
Microsoft.AspNetCore.Identity.PasswordHasher<TUser> (the default) |
PBKDF2 HMAC-SHA-512, 100k iterations | โ | semi (version byte prefix) | โ | n/a (it is the default) | every .NET they support |
Konscious.Security.Cryptography.Argon2 (raw) |
Argon2i / Argon2d / Argon2id | โ | โ (raw bytes) | manual | โ | netstandard2.0 |
BCrypt.Net-Next |
bcrypt | โ | โ
($2a$โฆ) |
โ | community wrappers | netstandard2.0 |
Isopoh.Cryptography.Argon2 |
Argon2i / Argon2d / Argon2id | โ | โ (encoded) | โ | โ | netstandard2.0 |
When this library is the right choice: you want Argon2id specifically
(per OWASP's current top recommendation for password storage), with
self-describing PHC strings so you can raise the work factor without
breaking existing users, optional first-class pepper with rotation,
and a drop-in IPasswordHasher<TUser> so an existing ASP.NET Core
Identity app can migrate with one line.
When the default PasswordHasher<TUser> is the right choice: you're in
a FIPS-enforced deployment (Argon2id is not FIPS-approved; see
COMPLIANCE.md). For everything else, Argon2id is the
modern best answer.
Performance characteristics
The numbers below are starting points on typical 2024 server hardware
โ measure on yours before you ship. See
OPERATIONS.md for the full envelope, capacity model,
and monitoring guidance.
| Parameter set | Per-hash time | RAM per in-flight hash | Recommended for |
|---|---|---|---|
m = 19 456, t = 2 (OWASP minimum) |
~30โ60 ms | ~19 MiB | Floor; not recommended |
m = 65 536, t = 3 (library default) |
~150โ250 ms | ~64 MiB | Consumer SaaS, general-purpose |
m = 131 072, t = 4 |
~400โ600 ms | ~128 MiB | Internal / B2B, latency-tolerant |
m = 262 144, t = 5 |
~800 ms โ 1.2 s | ~256 MiB | High-value (banking, healthcare) |
Verification time tracks hash time within ~5%. The library emits
metrics under
Argon2idDiagnostics.MeterName
so you can chart this directly in your observability stack:
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddMeter(Argon2idDiagnostics.MeterName));
Trim and Native AOT publish is supported; both packages are marked
IsTrimmable and IsAotCompatible.
Security posture
| Concern | How this library handles it |
|---|---|
| GPU / ASIC cracking | Argon2id, memory-hard (64 MiB default) |
| Rainbow tables | 128-bit cryptographically random salt per hash (RandomNumberGenerator) |
| Parameter drift | Parameters embedded in the PHC string + NeedsRehash |
| Timing side channels | FixedTimeEquals on the final comparison |
| Sensitive memory | Password, salt, and candidate hash buffers zeroed with CryptographicOperations.ZeroMemory; Span overloads avoid string |
| Database-only leak | Optional keyed pepper (secret kept outside the DB), with rotation |
| Algorithm confusion | Verifier accepts only argon2id, version 19 |
| Insecure config | Below-minimum parameters throw at construction |
| Supply chain | SourceLink, deterministic builds, NuGetAudit at build time, CodeQL, build-provenance attestations on every release |
This library is one layer. It does not provide rate limiting, account lockout, breached-password
checks, or MFA โ those belong at your application/identity layer. For a frank account of everything
it does not do (plaintext string lifetime, memory-cost DoS, and more), read
KNOWN-GAPS.md. Transparency is a feature.
API reference
Argon2idPasswordHasher
Argon2idPasswordHasher() // recommended defaults, no pepper
Argon2idPasswordHasher(Argon2idOptions options) // custom parameters
Argon2idPasswordHasher(Argon2idOptions options, PepperRing? pepper)
string HashPassword(string password)
string HashPassword(ReadOnlySpan<char> password)
string HashPassword(ReadOnlySpan<byte> password)
bool VerifyPassword(string password, string encodedHash)
bool VerifyPassword(ReadOnlySpan<char> password, string encodedHash)
bool VerifyPassword(ReadOnlySpan<byte> password, string encodedHash)
VerifyResult Verify(string password, string encodedHash) // single-parse: returns Success + NeedsRehash
VerifyResult Verify(ReadOnlySpan<char> password, string encodedHash)
VerifyResult Verify(ReadOnlySpan<byte> password, string encodedHash)
bool NeedsRehash(string encodedHash)
Argon2idOptions Options { get; }
VerifyResult (readonly record struct) โ Success, NeedsRehash,
static Failed. Returned by Verify(...).
Argon2idOptions (record) โ MemorySizeKib, Iterations, DegreeOfParallelism,
SaltSizeBytes, HashSizeBytes, Validate(), static Recommended.
Pepper โ Pepper(string id, byte[] key), string Id.
PepperRing โ PepperRing(Pepper active, params Pepper[] retired), Pepper Active.
Argon2id.PasswordHasher.AspNetCore โ Argon2idPasswordHasher<TUser> : IPasswordHasher<TUser>
and IServiceCollection.AddArgon2idPasswordHasher<TUser>(options?, pepper?).
The full public surface is locked by Microsoft.CodeAnalysis.PublicApiAnalyzers;
see PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt next to each csproj.
FAQ
Which Argon2 variant? Argon2id only โ RFC 9106's recommended variant. Argon2i/Argon2d are intentionally not offered, and the verifier rejects them.
Are hashes interoperable with other Argon2 libraries? Yes for the standard form โ it's the same
PHC string libsodium and the reference implementation use. The optional keyid parameter is a PHC
extension some parsers may not expect.
Do I need to store the salt separately? No. The salt (and all parameters) are part of the returned string. Store that single value.
How slow should hashing be? Aim for roughly 100โ500 ms per hash on your production hardware for
interactive logins. Benchmark and tune โ see docs/parameter-tuning.md.
Can I hash API keys / tokens with this? It's designed for human passwords. High-entropy secrets don't need memory-hard hashing; a fast keyed hash (HMAC/SHA-256) is usually the right tool.
Building, testing & benchmarks
dotnet build -c Release
dotnet test -c Release
# Run the benchmarks (Release only)
dotnet run -c Release --project benchmarks/Argon2id.PasswordHasher.Benchmarks
# Run the Blazor demo
dotnet run --project samples/Argon2id.PasswordHasher.Demo
Repository layout:
src/Argon2id.PasswordHasher/ core library
src/Argon2id.PasswordHasher.AspNetCore/ IPasswordHasher<TUser> adapter + DI
tests/ xUnit test projects
benchmarks/ BenchmarkDotNet harness
samples/Argon2id.PasswordHasher.Demo/ runnable Blazor Server demo (hardened)
samples/Argon2id.PasswordHasher.WasmDemo/ Blazor WebAssembly demo (deployed to GH Pages)
docs/ tuning & design notes
.github/ CI, CodeQL, Dependabot, templates
CI builds and tests on every push/PR across Ubuntu, Windows, and macOS for all three TFMs.
CodeQL scans run weekly. Tagged releases (v*) pack both packages, generate CycloneDX SBOMs,
issue build-provenance attestations, and create a GitHub Release with all artifacts attached.
NuGet publication is a separate manual CLI step โ see PUBLISHING.md.
Versioning & status
0.4.0-preview.2 โ preview. Follows SemVer with preview suffixes. The API and defaults may
still change before 1.0.0; hashes use the standard PHC format and are expected to stay verifiable.
See CHANGELOG.md for the full version history.
Contributing & security
Issues and PRs are welcome โ see CONTRIBUTING.md and
CODE_OF_CONDUCT.md. Security-relevant changes should update
SECURITY.md and/or KNOWN-GAPS.md.
Found a vulnerability? Please report it privately โ see SECURITY.md. Do not
open public issues for security reports.
License & acknowledgements
MIT ยฉ Paul Clark. See THIRD-PARTY-NOTICES.md for upstream
attributions.
Built on Konscious.Security.Cryptography.Argon2, an MIT-licensed managed Argon2 implementation. Parameter guidance follows RFC 9106 and the OWASP Password Storage Cheat Sheet.
<div align="center">
To God be the glory โ 1 Corinthians 10:31.
</div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- Argon2id.PasswordHasher (>= 0.4.0-preview.2)
- Microsoft.Extensions.Identity.Core (>= 10.0.0)
-
net8.0
- Argon2id.PasswordHasher (>= 0.4.0-preview.2)
- Microsoft.Extensions.Identity.Core (>= 8.0.0)
-
net9.0
- Argon2id.PasswordHasher (>= 0.4.0-preview.2)
- Microsoft.Extensions.Identity.Core (>= 9.0.0)
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 |
|---|---|---|
| 1.0.0 | 104 | 7/2/2026 |
| 0.4.0-preview.5 | 135 | 6/11/2026 |
| 0.4.0-preview.4 | 152 | 6/2/2026 |
| 0.4.0-preview.3 | 61 | 5/31/2026 |
| 0.4.0-preview.2 | 76 | 5/30/2026 |