CodeLogic.TwoFactorAuth
5.0.121
dotnet add package CodeLogic.TwoFactorAuth --version 5.0.121
NuGet\Install-Package CodeLogic.TwoFactorAuth -Version 5.0.121
<PackageReference Include="CodeLogic.TwoFactorAuth" Version="5.0.121" />
<PackageVersion Include="CodeLogic.TwoFactorAuth" Version="5.0.121" />
<PackageReference Include="CodeLogic.TwoFactorAuth" />
paket add CodeLogic.TwoFactorAuth --version 5.0.121
#r "nuget: CodeLogic.TwoFactorAuth, 5.0.121"
#:package CodeLogic.TwoFactorAuth@5.0.121
#addin nuget:?package=CodeLogic.TwoFactorAuth&version=5.0.121
#tool nuget:?package=CodeLogic.TwoFactorAuth&version=5.0.121
CodeLogic.TwoFactorAuth
TOTP two-factor authentication with QR-code generation for CodeLogic 5 — compatible with Google Authenticator, Authy, and 1Password.
Implements RFC 6238 time-based one-time passwords over Otp.NET and renders provisioning QR codes with QRCoder. Two services are exposed: an authenticator (generate/validate codes) and a QR-code generator.
Install
dotnet add package CodeLogic.TwoFactorAuth
Quick start
using CL.TwoFactorAuth;
using CL.TwoFactorAuth.Models;
using CodeLogic.Core.Results;
await Libraries.LoadAsync<TwoFactorAuthLibrary>(); // register before ConfigureAsync()
await CodeLogic.ConfigureAsync();
await CodeLogic.StartAsync();
var tfa = Libraries.Get<TwoFactorAuthLibrary>();
// 1. Enrol: create a key bound to issuer + user (fresh Base32 secret)
TwoFactorKey key = tfa.GenerateNewKey("MyApp", "user@example.com");
// persist key.SecretKey alongside the user record
// 2. Show a QR code for the authenticator app
Result<string> qr = tfa.GenerateQrCodeDataUri(key); // data:image/png;base64,...
if (qr.IsSuccess) { /* <img src="qr.Value"> */ }
// 3. Verify a code the user typed. Pass the user id: a code that was already used for that user is refused.
Result<TotpMatch> check = tfa.ValidateTotp("123 456", key.SecretKey, userId: "user@example.com");
var message = check switch
{
TotpMatch m => $"Verified (step {m.MatchedWindow})",
UnauthorizedError { Code: TwoFactorAuthErrorCodes.CodeAlreadyUsed } e => e.Message, // "This code has already been used."
UnauthorizedError e => e.Message, // 2fa.invalid_code: "Invalid code"
Error e => e.Message, // 2fa.validation_failed: "The code could not be validated."
null => "No result",
};
// At sign-in, LoginGuard adds attempt limits (five per user per five minutes by default).
// Every refusal (wrong, replayed, locked out) is the same UnauthorizedError 2fa.invalid_code.
Result<TotpMatch> login = tfa.LoginGuard.Validate("123 456", key.SecretKey, "user@example.com");
if (login.IsFailure) { /* deny: show login.Error!.Message */ }
Limit login attempts. A six-digit code has a million values and a drift window of three steps gives an attacker roughly three chances per guess, so unlimited attempts break TOTP in hours.
tfa.LoginGuardcounts attempts per user and locks out after five failures in five minutes;ValidateTotpdoes not count attempts. On several servers, supply a sharedITotpAttemptStore. Also rate-limit by source address, log failed attempts, and never reveal which factor failed.
Features
- Secret generation — cryptographically random Base32 TOTP secrets (
GenerateSecretKey). - Provisioning —
GenerateNewKey(issuer, user)returns aTwoFactorKeywith a standardotpauth://ProvisioningUrithat carries the configuredperiod; issuer, user and secret are percent-encoded. - Generate & validate —
GenerateTotpCode/ValidateTotpwith a configurable time step and drift window; whitespace in a typed code is ignored. Validation returns aResult<TotpMatch>: on success the matched absolute time step (MatchedWindow, along) and the drift in steps (WindowOffset); otherwise anErrorwith aTwoFactorAuthErrorCodescode. Errors are logged; the message is safe to show a user and never exception text. - Replay protection — with a
userId, a code whose time step was already accepted for that user is refused (UnauthorizedError2fa.code_already_used, "This code has already been used."). The check is atomic in anITotpUsedStepStore; the default keeps the last accepted step per user in memory, and a shared implementation serves several servers. - Login guard —
LoginGuard.Validate(code, secret, userId)combines replay protection with per-user attempt limits (ITotpAttemptStore; defaultInMemoryTotpAttemptStore, five attempts per five minutes). Wrong, replayed and locked-out codes give the same2fa.invalid_codeerror. - Lifetime helpers —
GetSecondsUntilExpiry()andGetCurrentTimeWindow(). - QR rendering — PNG / BMP bytes, Base64, data URI, and save-to-file (fully qualified
.pngpath), with configurable module size and error-correction level. Failures carry a generic message; details go to the log. - Events —
SecretKeyGeneratedEventandTotpValidatedEventon the CodeLogic event bus.
Errors
Every failure is an Error whose Code is a TwoFactorAuthErrorCodes constant; each code always has the same kind.
| Code | Constant | Kind | When |
|---|---|---|---|
2fa.invalid_code |
InvalidCode |
UnauthorizedError |
The code does not match ("Invalid code"); also every LoginGuard refusal. |
2fa.code_already_used |
CodeAlreadyUsed |
UnauthorizedError |
The code's time step was already accepted for the user ("This code has already been used."). |
2fa.validation_failed |
ValidationFailed |
InternalError |
Validation could not run, e.g. a malformed secret ("The code could not be validated."). |
2fa.generate_failed |
GenerateFailed |
InternalError |
GenerateTotpCode with an invalid secret. |
2fa.qr_png_failed, 2fa.qr_bmp_failed, 2fa.qr_save_failed |
QrPngFailed, QrBmpFailed, QrSaveFailed |
InternalError |
Rendering or writing a QR code failed; Details is the exception type. |
2fa.qr_invalid_path |
QrInvalidPath |
ValidationError |
SaveQrCodeToFile path is not fully qualified or not .png. |
Configuration
Auto-generated on first run as config.twofactorauth.json. Keys are camelCase and matched case-sensitively, so a PascalCase key is ignored and its default stays active:
{
"enabled": true,
"timeStepSeconds": 30,
"windowSize": 1,
"qrCodeModuleSize": 20,
"errorCorrectionLevel": "q"
}
| Setting | Default | Description |
|---|---|---|
Enabled |
true |
Master switch; when false the services aren't created and health reports disabled. |
TimeStepSeconds |
30 |
TOTP time step (1–300); must match the authenticator app. |
WindowSize |
1 |
Steps before/after the current window accepted (0–10); 1 ≈ ±30s drift. |
QrCodeModuleSize |
20 |
Pixel size of each QR module (1–100). |
ErrorCorrectionLevel |
Q |
L (~7%), M (~15%), Q (~25%), H (~30%). |
Values outside these ranges fail configuration validation.
Documentation
Full guide: CL.TwoFactorAuth documentation
Requirements
- CodeLogic 5.0+ · .NET 11
- Otp.NET 1.x · QRCoder 1.x
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CodeLogic.TwoFactorAuth:
| Package | Downloads |
|---|---|
|
CodeLogic.WebLogic
Web application toolkit for CodeLogic 5 — routing, templates, forms, an ordered security pipeline, auth flows and realtime. No third-party dependencies beyond CodeLogic.*. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.0.121 | 82 | 11/10/2026 |
| 5.0.118-preview | 74 | 11/8/2026 |
| 5.0.117-preview | 74 | 11/7/2026 |
| 5.0.116-preview | 73 | 11/1/2026 |
| 5.0.115-preview | 129 | 11/1/2026 |
| 5.0.113-preview | 45 | 10/31/2026 |
| 4.9.98 | 611 | 10/25/2026 |
| 4.8.93 | 73 | 9/23/2026 |
| 4.8.87 | 86 | 9/13/2026 |
| 4.8.85 | 64 | 9/12/2026 |
| 4.6.72 | 233 | 6/20/2026 |
| 4.6.69-preview | 66 | 6/20/2026 |
| 4.5.2 | 143 | 5/24/2026 |
| 4.5.2-preview.68 | 94 | 6/20/2026 |
| 4.5.1 | 189 | 5/24/2026 |
| 4.5.1-preview.56 | 134 | 5/24/2026 |
| 4.4.2-preview.53 | 89 | 5/24/2026 |
| 4.4.1 | 131 | 5/24/2026 |
| 4.0.5 | 138 | 5/15/2026 |
| 4.0.4 | 147 | 5/9/2026 |
# CL.TwoFactorAuth — Changelog
All notable changes to **CodeLogic.TwoFactorAuth** are documented here. Versions follow
[Semantic Versioning](https://semver.org/).
## 5.0 (2026-11-10)
### Breaking
- Targets **.NET 11** (`net11.0`) and requires **CodeLogic 5.0** (`[5.0.0, 6.0.0)`). On .NET 10, stay on
4.9.x. CodeLogic 5 makes `Result<T>` / `Result` C# 15 unions and `Error` a closed record with
kind subtypes (`NotFoundError`, `ValidationError`, ...). Results can now be matched exhaustively
with `switch`.
- **`TotpValidationResult` is removed; TOTP validation returns CodeLogic `Result<TotpMatch>`.** A refused code is
an `Error` whose `Code` is a `TwoFactorAuthErrorCodes` constant. Messages stay safe to show a user and never
contain exception text.
| Old | New |
|---|---|
| `TotpValidationResult TwoFactorAuthenticator.ValidateTotp(string, string, string?)` | `Result<TotpMatch>` |
| `TotpValidationResult TwoFactorAuthenticator.ValidateTotp(string, string, DateTime, string?)` | `Result<TotpMatch>` |
| `TotpValidationResult TotpLoginGuard.Validate(string, string, string)` | `Result<TotpMatch>` |
| `TotpValidationResult TwoFactorAuthLibrary.ValidateTotp(string, string, string?)` | `Result<TotpMatch>` |
| `result.IsValid` | `result.IsSuccess` |
| `result.MatchedWindow` / `result.WindowOffset` (`long?` / `int?`) | `TotpMatch.MatchedWindow` / `TotpMatch.WindowOffset` (`long` / `int`, via `result.Value` or a `switch`) |
| `result.ErrorMessage` `"Invalid code"` (no match) | `UnauthorizedError` `2fa.invalid_code`, message `"Invalid code"` |
| `TotpValidationResult.ReplayedCodeMessage` | `UnauthorizedError` `2fa.code_already_used`, message `"This code has already been used."` |
| `TotpValidationResult.ValidationErrorMessage` (malformed secret) | `InternalError` `2fa.validation_failed`, message `"The code could not be validated."`, the exception type in `Details` |
| `TotpLoginGuard.Validate` refusals (wrong, replayed, locked out, unreadable secret) | one `UnauthorizedError` `2fa.invalid_code` for all of them, as before |
| `TotpValidationResult.Valid(...)` / `Invalid(...)` | removed |
The inline codes `2fa.generate_failed`, `2fa.qr_png_failed`, `2fa.qr_bmp_failed`, `2fa.qr_save_failed` and
`2fa.qr_invalid_path` keep their values and are now `TwoFactorAuthErrorCodes` constants (`GenerateFailed`,
`QrPngFailed`, `QrBmpFailed`, `QrSaveFailed`, `QrInvalidPath`). New codes: `2fa.invalid_code`,
`2fa.code_already_used`, `2fa.validation_failed`.
- **`TotpValidationResult.MatchedWindow` is a `long?` holding the absolute time step** that matched
(Unix time / `TimeStepSeconds`, what Otp.NET reports and what `GetCurrentTimeWindow()` returns). It
was an `int?` documented as a drift offset, and the narrowing could overflow with short periods. Use it
for replay tracking (store the last accepted step per user, atomically). `TotpValidatedEvent.MatchedWindow`
is `long?` too, and `TotpValidationResult.Valid(...)` takes `(long? matchedWindow, int? windowOffset)`.
- **Configuration validation enforces the documented upper limits:** `TimeStepSeconds` 1–300,
`WindowSize` 0–10, `QrCodeModuleSize` 1–100. Only the lower bounds were checked before, so a config with
larger values loaded and now fails validation.
- **`ValidateTotp` with a `userId` refuses replayed codes.** A code whose time step is not greater than the last
step accepted for that user (the same code again, or an older step after a newer one) now fails with
`IsValid = false` and `ErrorMessage = TotpValidationResult.ReplayedCodeMessage` ("This code has already been
used."), and the `TotpValidatedEvent` reports `IsValid = false`. The check is atomic in the new
`ITotpUsedStepStore` (default `InMemoryTotpUsedStepStore`, one per process; the library keeps one across its
lifetime). Without a `userId` the behaviour is unchanged: the code is only checked against the secret.
Applications that tracked `MatchedWindow` themselves can keep doing so or drop that code.
- **`SaveQrCodeToFile` validates the path.** It accepted any string; the path must now be fully qualified and end in
`.png` (any case), otherwise it fails with `ValidationError` `2fa.qr_invalid_path` and nothing is written.
- **QR code errors no longer carry exception text.** `2fa.qr_png_failed`, `2fa.qr_bmp_failed` and
`2fa.qr_save_failed` returned `ex.Message` (which can describe the key or the file path); they now carry a fixed
message, `Details` is the exception type name, and the exception is logged.
### Added
- `TotpValidationResult.WindowOffset` (`int?`): the drift in steps (0 = current step, -1 = previous, +1 = next).
- `ValidateTotp(code, secretKey, DateTime timestampUtc, userId)` validates as of a given moment.
- `TwoFactorKey.PeriodSeconds` (default 30) and the `MinTimeStepSeconds` / `MaxTimeStepSeconds` /
`MaxWindowSize` / `MaxQrCodeModuleSize` constants on `TwoFactorAuthConfig`.
- `ITotpUsedStepStore` (`bool TryUse(userId, step)`) and `InMemoryTotpUsedStepStore` (retention two hours by
default, longer than any drift window the configuration allows), plus the `usedSteps` parameter on the
`TwoFactorAuthenticator` constructor for a shared (database, cache) store on several servers.
- `TotpValidationResult.ReplayedCodeMessage`.
- `TwoFactorAuthLibrary.ValidateTotp(code, secretKey, userId)` passes the user id through to the authenticator.
- `ValidateTotp` ignores whitespace in the typed code: `"123 456"` and `" 123456 "` validate as `"123456"`.
### Fixed
- **Validation errors no longer leak exception text.** `ValidateTotp` returned `"TOTP validation error: <exception
message>"` as `ErrorMessage` and `GenerateTotpCode` put the exception message and type in its `Error`. Both now return a
generic message (`TotpValidationResult.ValidationErrorMessage`, new constant) and log the details.
- `TwoFactorAuthenticator` and `QrCodeGenerator` throw `ArgumentNullException` for a `null` config.
- **`ProvisioningUri` escapes the secret.** The issuer and user were escaped but the secret was spliced raw, so a
padded (`=`) or hostile secret could add or override query parameters. It is now percent-encoded too.
- **The provisioning URI carries the time step.** `ProvisioningUri` omitted `period`, so with a
non-default `TimeStepSeconds` the authenticator app generated 30-second codes the server rejected.
`GenerateNewKey` now sets `PeriodSeconds` from the configuration and the URI always ends in
`&period=<seconds>`, which also changes the QR code content.
### Documentation
- Configuration examples use the camelCase keys the CodeLogic loader reads (PascalCase keys were ignored).
- The README and the guide state that the library does not rate-limit and that the application must throttle and
lock out failed attempts per user; the replay section describes the built-in store.
- The README describes `LoginGuard` (it no longer says the library never counts attempts) and its quick start adds
the usings it needs. The guide's services table lists `LoginGuard` instead of a `GenerateTotpCode` pass-through
the library does not have (it is on `Authenticator`). `TotpValidationResult` shows its `init` accessors, and
`SecretKeyGeneratedEvent` is documented as published by `GenerateNewKey` only.
- The README's last line, a relative link that broke on nuget.org, is removed.
### Changed
- Reject unsafe replay-store retention/intervals. Add TotpLoginGuard and bounded atomic ITotpAttemptStore / InMemoryTotpAttemptStore for application-controlled login lockout.
- Package versions are pinned (Otp.NET 1.4.1, QRCoder 1.8.1) instead of floating (`1.*`).
- The library manifest's description says CodeLogic 5 (it said CodeLogic 4).
## 2026-10-24
### Changed
- Requires **CodeLogic 4.9+**. The official libraries move to the 4.9 line together with the framework,
whose live config changes the stateful libraries now use. No API changes in this library.
## 2026-09-12
### Changed
- Unified the version line with the CodeLogic framework on **4.8.x**. Every official
library and the framework now share one `major.minor`, so a given `4.8.<patch>`
means the same generation across all packages.
- `version.txt` moved from `4.6` to `4.8`. The patch component remains the CI run
number, composed at pack time; `AssemblyVersion` stays pinned at `Major.Minor.0.0`
(now `4.8.0.0`) so every patch in the line loads interchangeably.
## 2026-06-20
### Documentation
- Full rewrite of the README and the `docs/libs/twofactorauth.md` guide to the
shared house style: a concise, NuGet-rendered README (badges, tagline, install,
quick start, features, configuration table + JSON, requirements) and a single
deep documentation page covering the enrolment flow, sign-in validation, the
`Authenticator` and `QrCode` services, the `TwoFactorKey` /
`TotpValidationResult` shapes, configuration, events, and the health check.
No API changes.
## [4.5.2] — 2026-06-20
### Documentation
- Corrected the README and the security guide to match the shipping API. The
previous docs referenced members and config keys that do not exist
(`GenerateSecret`, `Validate(secret, token)`, `GetOtpAuthUri`, backup-code
helpers, and the `Issuer`/`SecretKeyLength`/`TokenValiditySeconds`/
`AllowedClockSkewSeconds`/`QrCodeSize` settings).
- Documented the real configuration keys: `Enabled`, `TimeStepSeconds`,
`WindowSize`, `QrCodeModuleSize`, and `ErrorCorrectionLevel`
(`L`/`M`/`Q`/`H`).
- Documented the `Authenticator` and `QrCode` services and their full surface:
`GenerateSecretKey`, `GenerateNewKey`, `GenerateTotpCode`, `ValidateTotp`
(incl. the optional `userId` argument and the `TotpValidationResult` shape),
`GetSecondsUntilExpiry`, `GetCurrentTimeWindow`, and the QR outputs
`GenerateQrCodePng` / `GenerateQrCodeBmp` / `GenerateQrCodeBase64` /
`GenerateQrCodeDataUri` / `SaveQrCodeToFile`.
- Documented the `TwoFactorKey.ProvisioningUri` (`otpauth://`) property, the
`SecretKeyGeneratedEvent` / `TotpValidatedEvent` bus events, and the actual
health-check behavior (live TOTP key generation).
## [4.5.0] — 2026-05-24
### Changed
- **Unified versioning.** All CodeLogic.Libs now share a single version line
controlled by `version.txt` in the repo root. This is a version alignment
release — no functional changes to this library.
## [4.0.4] — 2026-04-16
### Changed
- README + manifest refresh for the v4 baseline. No functional changes vs 4.0.3.
- `LibraryManifest.Version` now reads from assembly metadata.
## [4.0.2] — 2026-04-09
### Changed
- Annotated 2FA configuration with `[ConfigField]` for the admin UI surface.
- Aligned with the v4 baseline across all libraries.
## [4.0.0] — 2026-04-09
Major rewrite. Republished as v4.0.0 to reset the version line under the
unified v4 baseline. TOTP-based 2FA with QR code generation, backup codes,
and a CodeLogic-native flow for Google Authenticator / Authy / 1Password.
### Notes
- Earlier history is retained in the
[git log](https://github.com/zyntal-com/CodeLogic.Libs/commits/main/CL.TwoFactorAuth).