full 2FA
[0.6.1]
Patch: fixes and a refactor from a live end-to-end run + multi-agent code review. No contract change —
OpenAPI stays at 0.6.0.
Fixed
- Enum wire format —
TwoFactorMethodTypenow serializes as a string ("Totp") via an attribute on the enum, matching the OpenAPI contract and the TS types regardless of host JSON settings. Previously emitted as an integer, silently breaking everytype === 'Totp'check on the frontend. - Rate limiting — the shared window now lives in a singleton (
Omni2FaRateLimiter) resolved per request, so all sensitive endpoints actually share one IP partition. Previously the limiter never engaged. - Example 2FA bypass — the host example's
POST /auth/finalizenow derives the user from the validated pre-auth token instead of trusting auserIdin the request body (which let anyone mint a session for any user). - Validate
Omni2Fa:PreAuth:SigningKeylength at startup (ValidateOnStart); backgroundChallengePurgeBackgroundServiceprunes expired challenges; unique index + uncapped length on WebAuthn credential columns; token routing classifies endpoints by path under the mount, not a URL substring; example enables forwarded headers for correct client IP behind a proxy.
Refactor
- Extracted the shared enrollment tail (first-method recovery codes +
MethodEnrolledaudit) intoIEnrollmentFinalizer, removing the duplication across the three enrollment services. - Packaging: folded the standalone
Omni2FA.WebAuthnpackage intoOmni2FA.AspNetCore— the .NET side now ships 3 NuGet packages instead of 4. TheIWebAuthnCeremonyServicecontract stays inOmni2FA.Core(core remains FIDO2-free); only the Fido2NetLib implementation moved into the adapter. Consumers still just installOmni2FA.AspNetCore(+ the EF store).
[0.6.0]
Production-hardening release: recovery codes (v0.4 scope) plus the v0.6 stabilization items, so
Omni2FA can be deployed to a real app for testing. The v0.5 @omni2fa/react-mui styled package is
intentionally deferred — hosts use the headless @omni2fa/react hooks (see the example).
Added
Recovery codes (.NET)
- One-time backup codes: generated on first method enrollment (returned once in
MethodCreatedResponse.recoveryCodes),POST /recovery-codes/regenerate, andPOST /challenge/recovery-codeas a method-agnostic login fallback.XXXX-XXXX-XXformat, SHA-256 hashed at rest, one-time use. NewRecoveryCodeentity +IRecoveryCodeStore(EF adapter, configurableRecoveryCodesTableName) +IRecoveryCodeService. Wiped when a user's last method is removed.
Hardening (.NET)
- Rate limiting — IP-partitioned fixed-window limiter (
RateLimitFilter, default 20/min/IP) on the sensitive endpoints (challenge verify/resend/recovery-code, enroll groups). Self-contained — no hostUseRateLimiter. Returns429 TOO_MANY_ATTEMPTS+Retry-After. Configurable viaOmni2Fa:RateLimit. - Audit —
IOmni2FaAuditSinkraising MethodEnrolled/Removed, LoginVerifySucceeded/Failed, RecoveryCodes{Generated,Regenerated}, RecoveryCodeUsed, RateLimitExceeded. DefaultLoggerAuditSink(structuredILogger) registered viaTryAdd; hosts replace it. - Last-method policy —
AspNetCore.AllowDisablingLastMethod(default true); when false, removing the last method returns409 LAST_METHOD_PROTECTED.
Client (@omni2fa/core)
- Session-token API —
setSessionToken/getSessionToken,credentialsconfig,sessionStorageKey. The request middleware now routes the pre-auth token to/challenge/*and the host session token to everything else, removing the custom-fetch workaround.regenerateRecoveryCodes/verifyRecoveryCodeclient methods; challenge-machine recovery-code branch; enrollment machines surfacerecoveryCodes.
React (@omni2fa/react)
useChallengegainsuseRecoveryCode.
Protocol
- OpenAPI
0.6.0: recovery-code endpoints +MethodCreatedResponse.recoveryCodes.
Example (examples/full)
- Recovery codes shown once after first enrollment, regenerate button, recovery-code login path.
omni2fa.tssimplified to use the session-token API (custom fetch removed).
[0.3.0]
Added
.NET
- WebAuthn (passkeys & FIDO2 security keys) enrollment endpoints:
POST /enroll/webauthn/start(issues creation options) andPOST /enroll/webauthn/confirm(verifies the attestation). WebAuthn login via/challenge/start(issues assertion options) and/challenge/verify(validates the assertion, updates the signature counter). Omni2FA.WebAuthnproject filled in:Fido2WebAuthnCeremonyServiceon Fido2NetLib 4.x — resident keys, sign-count tracking, globally-unique credential ids, per-user cap (MAX_METHODS_REACHED).IWebAuthnCeremonyServicein core (FIDO2-free interface + value objects) so orchestration never depends on the crypto library;WebAuthnEnrollmentServiceorchestrates ceremony + stores.WebAuthnOptions(RP id/name, allowed origins,MaxCredentialsPerUser) bound underOmni2Fa:WebAuthn.ChallengeVerifyRequest.assertionResponseJsonandChallengeStartResponse.optionsJsonadded;codeis now optional.
TypeScript core (@omni2fa/core)
- WebAuthn browser marshaling (
startRegistration,startAuthentication) — base64url ↔ ArrayBuffer,navigator.credentials.create/get. webauthnEnrollmentMachine(start → auto browser ceremony → confirm) and challenge-machine WebAuthn branch (auto-assert on pick). Client methodsstartWebAuthnEnrollment,confirmWebAuthnEnrollment.
React adapter (@omni2fa/react)
useWebAuthnEnrollment+useWebAuthnEnrollmentSelector.
Protocol
- OpenAPI bumped to
0.3.0with the WebAuthn enrollment endpoints and assertion fields.
Example (examples/full)
- Passkey enrollment dialog and passkey login path.
Omni2Fa:WebAuthnconfigured forlocalhost/http://localhost:5173.
Changed
- TypeScript DTO aliases consolidated from one file each into a single
types/dtos.tsbarrel (pure generated-type aliases aren't "concepts" — seedocs/CODE_STYLE.mdrule 1).
[0.2.0]
Added
.NET
- Email OTP enrollment endpoints under
/api/2fa:POST /enroll/email/start,POST /enroll/email/confirm,POST /enroll/email/resend. The host supplies the destination address in the request body — Omni2FA does not read it from a claim and does not own address verification. - Email OTP login:
POST /challenge/startissues and sends a code for Email methods;POST /challenge/resendre-sends it (cooldown-guarded);POST /challenge/verifyvalidates it. IEmailSender(transport) with a default MailKit/SMTP implementation registered viaTryAdd— hosts replace it with their own email infrastructure.IEmailMessageBuilder(copy) with a default English implementation, overridable for localization.- Background email delivery by default (
EmailOptions.BackgroundDelivery, on): OTP endpoints return without waiting on SMTP — messages queue onto an in-process channel drained by a hosted worker, send failures are logged, not surfaced. Set false for inline (awaited) send. Implemented viaIEmailDispatcher. IEmailOtpServiceprimitive (generate, hash, send, verify) andIEmailEnrollmentServiceorchestrator. Codes are SHA-256 hashed at rest; verification is constant-time.EmailOptions(digits, TTL, resend cooldown, sender identity, SMTP) bound underOmni2Fa:Email.TwoFactorMethod.EmailAddressandTwoFactorChallenge.EmailAddresscolumns;ITwoFactorChallengeStore.GetActiveLoginChallengeAsyncfor method-keyed login challenges.
TypeScript core (@omni2fa/core)
emailEnrollmentMachine(start → awaitingCode → confirming, with resend) and challenge-machine Email support (resend +expiresAt/resendAvailableAtin context).- Client methods
startEmailEnrollment,confirmEmailEnrollment,resendEmailEnrollment,resendChallenge.
React adapter (@omni2fa/react)
useEmailEnrollment+useEmailEnrollmentSelector.useChallengegains aresendaction.
Protocol
- OpenAPI bumped to
0.2.0with the Email enrollment endpoints,/challenge/resend, andexpiresAt/resendAvailableAtonChallengeStartResponse.
Example (examples/full)
- Switched from EF InMemory to SQLite (state survives restarts — needed for realistic challenge/login testing).
- Email OTP enrollment dialog and Email login path. SMTP points at a local catcher (
localhost:1025, e.g. Mailpit) by default.