Releases: BlackApplication/Omni2FA
v0.8.0
[0.8.0]
Extends step-up to the library's own sensitive endpoints. The endpoints MapOmni2Fa mounts (remove
method, regenerate recovery codes, enroll a new factor) can't be decorated by the host, so they're gated
by opt-in config flags, and the JS client confirms + retries them automatically. Minor bump: those
endpoints can now return 403 STEP_UP_REQUIRED, and the client API + config grow.
Added
- Per-action step-up flags (.NET) —
StepUp.RequireTwoFactorToEnroll/...ToRemoveMethod/...ToRegenerateRecoveryCodes(all defaultfalse). When on,MapOmni2Faapplies.RequireStepUp()to the matching endpoint (/enroll/*/start,DELETE /methods/{id},/recovery-codes/regenerate). A user with no active method is never blocked (NotEnrolledBypass). - Client-side step-up handling (
@omni2fa/core) —IOmni2FaClient.setStepUpHandler(handler): when one of the client's own sensitive calls returns403 STEP_UP_REQUIRED, the client invokes the handler (e.g. the ReactconfirmTwoFactor) and retries with theX-Omni2FA-StepUpheader. CoversremoveMethod,regenerateRecoveryCodes, and the three enroll-startcalls — whether called directly or via hooks.
Changed
- OpenAPI
0.8.0: the five gated operations document a403 STEP_UP_REQUIREDresponse (returned only when the host enabled the matching flag). - Docs (
README,ARCHITECTURE,ASPNETCORE,FLOWS) and theexamples/fullhost (flags on; a sharedStepUpDialog+ aStepUpModalHostthat registers the handler) updated.
Security
- Closes a gap from 0.7.3: the library's own destructive endpoints (notably remove-method and enroll-a-new-factor — a session-theft persistence vector) had no step-up path and couldn't be protected from outside. Recovery codes remain one-way hashed: there is no "view existing codes", so the gated recovery action is regenerate, not view.
v0.7.3
[0.7.3]
Adds step-up authentication — a strict, single-use 2FA confirmation gate for sensitive actions
(change password, view recovery codes, remove a method), independent of the login flow. OpenAPI moves
to 0.7.3; the .NET and all TypeScript packages bump to match.
Added
- Step-up barrier (.NET) —
[RequireTwoFactor](MVC action filter) and.RequireStepUp()(minimal-API endpoint filter) gate any endpoint: an enrolled user must present a valid step-up token or the call returns403 STEP_UP_REQUIRED(carrying the available methods); a user with no 2FA passes through. The decision lives once inOmni2FA.Core'sIStepUpEvaluator. New/api/2fa/stepup/start|resend|verifyendpoints (session-authenticated mirrors of/challenge/*);verifymints a single-use step-up token (purpose=2fa-stepup) via the newIPreAuthTokenIssuer.IssueStepUp/ValidateStepUp. NewStepUp.Ttl+ header-name options. - Single-use enforcement —
IStepUpNonceStorerecords spent token ids until expiry; defaultInMemoryStepUpNonceStoreis single-instance (register a shared store, e.g. Redis, for multi-node — otherwise a token spent on one node has a replay window on the others bounded by the TTL). The token is bound to the caller, so a stolen token can't be replayed against another account. - Step-up (
@omni2fa/core) —client.startStepUp/resendStepUp/verifyStepUp, thestepUpMachine, theSTEP_UP_HEADERconstant, and theSTEP_UP_REQUIREDerror code. Transport-agnostic by design — the library never makes the protected request, so cookie- and Bearer-session hosts integrate identically. - Step-up (
@omni2fa/react) —useStepUp()returningconfirmTwoFactor(methods)(shows the prompt, resolves a single-use token) plus the prompt state (active,methods,status,pick/submit/resend/cancel); reuses the existing challenge UI. The host detects403 STEP_UP_REQUIREDand replays the request with the header in its own fetch/axios layer.
Changed
ITwoFactorChallengeServicegainsVerifyStepUpAsync; login and step-up share one verification core (no behavior change to login).- Docs (
README,ARCHITECTURE,FLOWS,ASPNETCORE,ERROR_CODES) and theexamples/fullhost (a step-up-protectedPOST /user/change-password) updated.
[0.7.2]
Patch: EF Core 10 host compatibility. .NET packages only — no API contract change, so OpenAPI stays
at 0.7.1 and the TypeScript packages are unchanged.
Fixed
- EF Core 10 host compatibility (
MissingMethodExceptionon bulk delete) —Omni2FA.AspNetCore.EntityFrameworkCorenow multi-targetsnet8.0;net10.0, compiling each build against its matching EF Core major (8.0.x / 10.0.x). The previous singlenet8.0build boundExecuteDeleteAsyncto EF Core 8'sRelationalQueryableExtensions; under a host running EF Core 10 that method has moved, so recovery-code wipe and challenge purge threwMissingMethodExceptionat runtime. NuGet now hands each host the matching asset.Omni2FA.CoreandOmni2FA.AspNetCorestaynet8.0(consumed down-level by net10 hosts).
v0.7.1
[0.7.1]
Closes an email-enrollment foot-gun surfaced in live integration: the OTP destination was taken
verbatim from the request body, so every host had to remember to substitute an authoritative,
verified address — and any host that forgot enrolled whatever the caller sent. Omni2FA now derives
the address from the authenticated identity by default. OpenAPI moves to 0.7.1.
Added
IUserContextAccessor.GetCurrentUserEmail()— resolves the current user's email from the configuredAspNetCoreOptions.UserEmailClaim(defaultClaimTypes.Email, falling back to the raw JWTemailclaim). Kept distinct fromUserLabelClaimbecause the OTP destination is security-sensitive, not cosmetic.UserContextAccessormethods are nowvirtual, so a host with a non-claim source overrides this one method instead of writing endpoint glue.AspNetCoreOptions.EmailEnrollmentAddressSource—ClaimOnly(default) derives the address from the identity;HostSuppliedpreserves the previous body-supplied behavior for hosts that legitimately enroll an address other than the signed-in one.
Changed
POST /enroll/email/startis secure by default — underClaimOnlythe bodyemailis ignored and the address comes fromGetCurrentUserEmail().EmailEnrollStartRequest.Emailis now optional (was required); the TS client/machine and theuseEmailEnrollmenthook'sstart(email?)accept an omitted address accordingly.
Migration
- Hosts relying on the request-body address (e.g. a decorator that injected the user's email) can delete that glue — the default now does it. Hosts that intentionally enroll a different address than the identity claim must set
EmailEnrollmentAddressSource = HostSupplied.
Refactor
- Extracted repeated store idioms into
ChallengeStoreExtensions(GetActiveEnrollmentAsync,RecordFailedAttemptAsync,AddAndSaveAsync) and reused them across the Email/TOTP/WebAuthn enrollment services and the challenge service. Centralizes the "matching challenge kind" guard and the write-then-save pairs; no behavior change.UserContextAccessor's three claim lookups now share aFindClaimValuehelper.
v0.7.0
[0.7.0]
Compatibility release from live-integration feedback: gives hosts a clean "2FA actually passed" signal
instead of forcing them to reconstruct one from audit events. OpenAPI moves to 0.7.0.
Added
- Verified-handoff token —
challenge/verifyandchallenge/recovery-codenow return a short-livedverifiedToken(purpose=2fa-verified) alongsideuserId. The frontend forwards it to the host's finalize endpoint, which calls the newIPreAuthTokenIssuer.ValidateVerified(token)to recover the trusted user id and mint the session. NewIssueVerified/ValidateVerifiedonIPreAuthTokenIssuer;PreAuth.VerifiedTtloption (default 2 min).VerifySuccessResponsegainsverifiedToken+expiresAt; surfaced in the TS challenge machine ascontext.verifiedToken.
Fixed
- 2FA bypass in the host finalize pattern — finalize must validate the verified-handoff token, not the pre-auth token. The pre-auth token is minted right after the password step, so re-validating it (as the example previously did) let anyone who passed the password — but not 2FA — mint a session. The example now validates
verifiedToken. - Recovery-code "verified" signal — a recovery-code login previously emitted only the
RecoveryCodeUsedaudit event, so a host inferring success fromLoginVerifySucceededsilently rejected it. Both paths now return the sameverifiedToken, so recovery is no longer a special case. This also removes the per-user race in audit-based gates: the token is bound to the ceremony, not the user.
Changed
- Docs (
README,FLOWS,ASPNETCORE) and theexamples/fullhost updated to the verified-handoff finalize flow.
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.
v0.1.0 — TOTP end-to-end
Added
.NET
- ASP.NET Core Minimal API endpoints under
/api/2fa:GET /methods,DELETE /methods/{id},POST /enroll/totp/start,POST /enroll/totp/confirm,POST /challenge/start,POST /challenge/verify. AddOmni2Fa(...)DI extension andMapOmni2Fa()route extension inOmni2FA.AspNetCore.IUserContextAccessorwith default impl reading configurable claim fromHttpContext.User.- Custom
IEndpointFiltervalidating pre-auth Bearer tokens (no dependency on the host's authentication pipeline). Result→IResultmapping viaToHttpResult()extension with centralized error-code → HTTP-status switch.JwtPreAuthTokenIssuer(HMAC-SHA256, configurable issuer/audience/TTL).TotpServicebuilt onOtp.NET;DataProtectionSecretProtectorfor at-rest secret encryption.- EF Core store adapter in
Omni2FA.AspNetCore.EntityFrameworkCorewith configurable table names, schemas, and column lengths. - Stringified user identifier (
string UserId) — supports any host id type without generic spread.
TypeScript core (@omni2fa/core)
- Typed HTTP client over
openapi-fetch, auto-attachesAuthorization: Bearer <pre-auth>. IStorageabstraction withMemoryStorage(default),SessionStorageStorage,LocalStorageStorage.- Three xstate v5 machines:
totpEnrollmentMachine,challengeMachine,methodsMachine. createOmni2Fa({ baseUrl, storage })— one-call factory assembling client + actors.- DTO types auto-generated from the OpenAPI contract via
openapi-typescript. Omni2FaApiErrorcarrying stable error code + HTTP status + structured details.
React adapter (@omni2fa/react)
Omni2FaProvider+useOmni2Fa()context.useTotpEnrollment,useChallenge,useMethods— headless hooks with named action proxies.useTotpEnrollmentSelector,useChallengeSelector,useMethodsSelector— escape hatches for granular subscriptions.useMethods({ autoLoad })— auto-fetch on mount with opt-out.
Protocol
- OpenAPI 3.1 contract published in
Core/protocol/omni2fa.openapi.yaml. - Stable error code catalogue in
Core/protocol/ERROR_CODES.md.
Documentation
docs/ARCHITECTURE.md— framework-agnostic core / thin-adapter boundary rule with code review checklist.docs/ASPNETCORE.md— design decisions for the ASP.NET Core adapter.docs/CODE_STYLE.md,docs/FLOWS.md,docs/ROADMAP.md.