Omni2FA.AspNetCore 0.8.0

dotnet add package Omni2FA.AspNetCore --version 0.8.0
                    
NuGet\Install-Package Omni2FA.AspNetCore -Version 0.8.0
                    
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="Omni2FA.AspNetCore" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Omni2FA.AspNetCore" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Omni2FA.AspNetCore" />
                    
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 Omni2FA.AspNetCore --version 0.8.0
                    
#r "nuget: Omni2FA.AspNetCore, 0.8.0"
                    
#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 Omni2FA.AspNetCore@0.8.0
                    
#: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=Omni2FA.AspNetCore&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Omni2FA.AspNetCore&version=0.8.0
                    
Install as a Cake Tool

Omni2FA

Drop-in multi-method two-factor authentication for self-hosted apps — TOTP, Email OTP, WebAuthn/passkeys, recovery codes — behind one OpenAPI contract.

npm NuGet License Status

You verify the password and mint your session; Omni2FA handles everything 2FA in between — enrollment, the login challenge, OTP/WebAuthn ceremonies, recovery codes, persistence, rate limiting, audit. Backend service + endpoints + EF store on .NET; headless hooks on React. Other stacks implement the same contract (roadmap below).

For apps that own their user table (ASP.NET Core Identity, custom JWT/cookie auth, …). Not for managed identity providers (Auth0, Clerk, Cognito, Firebase, Supabase, Okta, WorkOS) — they already ship 2FA.


Packages

Stack Install Notes
.NET (ASP.NET Core) Omni2FA.AspNetCore + Omni2FA.AspNetCore.EntityFrameworkCore Omni2FA.Core comes transitively
React @omni2fa/core + @omni2fa/react hooks; styled MUI dialogs (@omni2fa/react-mui) — planned

Other backends (Node, Python) and frontends (Angular, Vue) are on the roadmap; any stack can implement the OpenAPI contract.


Backend — ASP.NET Core

dotnet add package Omni2FA.AspNetCore
dotnet add package Omni2FA.AspNetCore.EntityFrameworkCore

Program.cs

builder.Services.AddOmni2Fa(o => builder.Configuration.GetSection("Omni2Fa").Bind(o));
builder.Services.AddOmni2FaEntityFrameworkStore<AppDbContext>();   // or implement the store interfaces yourself

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapOmni2Fa();                                                  // mounts /api/2fa/*

Your DbContext

protected override void OnModelCreating(ModelBuilder modelBuilder) {
    base.OnModelCreating(modelBuilder);
    modelBuilder.ApplyOmni2FaConfiguration();                      // 2FA tables (names configurable)
}

Hook the pre-auth token into your existing login — the seam between your password check and the 2FA step:

public class AuthService(IPreAuthTokenIssuer preAuth, ITwoFactorMethodStore methods) {
    // after you verify the password:
    if (await methods.HasActiveMethodsAsync(userId)) {
        var ticket = preAuth.Issue(userId);                        // short-lived JWT
        var available = await methods.ListActiveByUserAsync(userId);
        return Challenge(ticket.Token, ticket.ExpiresAt, available); // your DTO → frontend
    }
    // else: mint your session as usual

    // when the frontend finishes 2FA, it calls your "finalize" with the verifiedToken from the verify response:
    var verifiedUserId = preAuth.ValidateVerified(verifiedToken);  // null if not actually verified → reject
}

Use the DTOs the library already ships at the host seam — don't reinvent them (all in Omni2FA.Core.Dtos / .Services):

DTO Where you use it
PreAuthTokenInfo returned by Issue / IssueVerified{ Token, ExpiresAt }
PreAuthChallengeResponse your login response when 2FA is required — { PreAuthToken, AvailableMethods, ExpiresAt }
TwoFactorMethodDto shape of an enrolled method (AvailableMethods items)
VerifySuccessResponse the /challenge/verify body — { UserId, VerifiedToken, ExpiresAt }
ErrorResponse + Omni2FaErrorCodes error envelope and the stable error-code constants

appsettings.json

"Omni2Fa": {
  "PreAuth":  { "SigningKey": "<32+ char HMAC key — from env/secrets>" },
  "Totp":     { "Issuer": "MyApp" },
  "Email":    { "FromAddress": "no-reply@myapp.com",
                "Smtp": { "Host": "smtp.example.com", "Port": 587, "Username": "...", "Password": "...", "UseStartTls": true } },
  "WebAuthn": { "RelyingPartyId": "myapp.com", "Origins": [ "https://myapp.com" ] },
  "StepUp":   { "RequireTwoFactorToEnroll": true, "RequireTwoFactorToRemoveMethod": true, "RequireTwoFactorToRegenerateRecoveryCodes": true }  // opt-in (default false): step-up on Omni2FA's own destructive endpoints
}

That's the whole backend: endpoints, the three methods, recovery codes, rate limiting and audit are live.


Frontend — React

npm i @omni2fa/core @omni2fa/react

Once, at the app root

import { createOmni2Fa } from '@omni2fa/core';
import { Omni2FaProvider } from '@omni2fa/react';

export const omni = createOmni2Fa({ baseUrl: '/api/2fa' });
// after your login, hand the client your host session token (or use cookies: createOmni2Fa({ credentials: 'include' })):
omni.client.setSessionToken(sessionToken);

<Omni2FaProvider value={omni}>{children}</Omni2FaProvider>

The hooks expose state + actions; you render the UI (headless).

Login challenge

import { useChallenge } from '@omni2fa/react';

const { status, context, pick, submit, useRecoveryCode } = useChallenge();
// pick(methodId) → submit(code)  (WebAuthn auto-runs the browser ceremony)
// status: 'idle' | 'awaitingCode' | 'asserting' | 'verifying' | 'verified' | 'failed' | …
// on 'verified': send context.verifiedToken to your finalize endpoint (not the pre-auth token)

Hooks: useMethods, useTotpEnrollment, useEmailEnrollment, useWebAuthnEnrollment, useChallenge (+ *Selector variants). A full headless UI you can copy lives in examples/full/frontend. Styled drop-in components (@omni2fa/react-mui) are planned.


Step-up — confirm sensitive actions

Force a fresh 2FA check right before a sensitive action (change password, view recovery codes, remove a method). Decorate the endpoint: if the user has 2FA enrolled they must confirm it; if they don't, the call passes through. A stolen session alone can't perform the action.

Backend — one attribute on an MVC action, or .RequireStepUp() on a minimal-API endpoint:

[HttpPost("change-password")]
[RequireTwoFactor]                         // → 403 STEP_UP_REQUIRED until a valid step-up token is sent
public Task<IActionResult> ChangePassword(ChangePasswordRequest req) { … }

// minimal API:
app.MapPost("/account/email", ChangeEmail).RequireAuthorization().RequireStepUp();

FrontenduseStepUp() gives you confirmTwoFactor(methods): it shows the 2FA prompt and resolves a single-use token (or null if cancelled). You attach that token in the X-Omni2FA-StepUp header on your request — over fetch or axios, cookie or Bearer session; the library never touches your transport. Two ways to use it:

Reactive — let the server tell you. Best in one central place (an axios/fetch interceptor, right next to your 401 handling) — covers every protected endpoint at once:

import { STEP_UP_HEADER, Omni2FaErrorCodes } from '@omni2fa/core';

// in your response interceptor, when a call returns 403:
const err = await res.clone().json();
if (err.code === Omni2FaErrorCodes.StepUpRequired) {
  const token = await confirmTwoFactor(err.details.availableMethods);
  if (token) res = await replayRequest({ [STEP_UP_HEADER]: token });   // retry with the header
}

Proactive — when you already know an action needs 2FA, confirm up-front and skip the 403 round-trip. You supply the methods yourself (e.g. from useMethods()):

const { confirmTwoFactor } = useStepUp();
const { items: methods } = useMethods();

async function changePassword() {
  let headers = {};
  if (methods.length > 0) {                       // no 2FA enrolled → nothing to confirm, server lets it through
    const token = await confirmTwoFactor(methods);
    if (!token) return;                           // cancelled
    headers = { [STEP_UP_HEADER]: token };
  }
  await api.changePassword(body, headers);        // sent already carrying the token
}

While a prompt is active, render the 2FA UI (reuse your challenge UI): methodspick(id)submit(code).

Protecting the library's own endpoints — remove method, regenerate recovery codes, and enroll a new factor are mounted by MapOmni2Fa, so you can't decorate them. Turn them on with the per-action StepUp.RequireTwoFactorTo* flags (in appsettings.json above; all off by default — and recovery codes can't be viewed, only regenerated, so that's the gated action). Then register the prompt once so the client handles those 403s itself (no per-call wiring):

const { confirmTwoFactor /* + prompt state to render */ } = useStepUp();
useEffect(() => omni.client.setStepUpHandler(confirmTwoFactor), [confirmTwoFactor]);

Now omni.client.removeMethod(...) / regenerateRecoveryCodes() / enrollment prompt for 2FA and retry automatically. A user with no method enrolled is never blocked.

The step-up token is single-use — one confirmed 2FA per protected action. Consumed token ids are kept in memory by default, so on a multi-instance deployment a token spent on one node isn't known to the others — a brief replay window within the token TTL. Register a shared IStepUpNonceStore (e.g. Redis) to close it. Details in docs/ARCHITECTURE.md.


Configuration (key options, under Omni2Fa)

Option Default Purpose
PreAuth.SigningKey — (required, ≥32 chars) HMAC key for the pre-auth ticket; validated at startup
PreAuth.Ttl 5 min Pre-auth ticket lifetime
PreAuth.VerifiedTtl 2 min Verified-handoff token lifetime (the finalize proof)
StepUp.Ttl 5 min Step-up token lifetime — gap allowed between confirming 2FA and the action
StepUp.RequireTwoFactorTo{Enroll,RemoveMethod,RegenerateRecoveryCodes} false Gate the library's own destructive endpoints with step-up (opt-in, per action)
Totp.Issuer Omni2FA Name shown in authenticator apps
Email.Smtp.* / Email.BackgroundDelivery — / true SMTP transport; codes sent on a background worker by default
WebAuthn.RelyingPartyId / Origins localhost / http://localhost:5173 Must match your real hostname (HTTPS off-localhost)
WebAuthn.MaxCredentialsPerUser 3 Passkey cap per user
RateLimit.{PermitLimit,Window} 20 / 1 min Per-IP limit on verify/enroll endpoints
AspNetCore.AllowDisablingLastMethod true Set false to forbid removing the last method
EF table/column names Omni2Fa* Override via ApplyOmni2FaConfiguration(o => …) for migrations

Extension points (swap any piece — TryAdd, host wins)

Interface Replace to…
IEmailSender send via your own infra (SendGrid/SES/relay) instead of MailKit/SMTP
IEmailMessageBuilder customize/localize the OTP email copy
IOmni2FaAuditSink forward audit events to your log/SIEM (default → ILogger)
ITwoFactorMethodStore / ITwoFactorChallengeStore / IRecoveryCodeStore use Mongo/Dapper/raw ADO instead of EF Core
IUserContextAccessor derive the current user id from a custom claim/header
IStepUpNonceStore share single-use step-up token ids across instances (Redis/DB) — default is in-memory
IPreAuthTokenIssuer change how the pre-auth ticket is minted/validated
IWebAuthnCeremonyService swap the FIDO2 implementation

Methods & status

Method State
TOTP (authenticator apps)
Email OTP (SMTP)
WebAuthn (passkeys / security keys)
Recovery codes (one-time, hashed)
Rate limiting · audit sink

🚧 Status — pre-1.0 (0.6.x). Functionally complete for .NET + React, verified by a live end-to-end run, but young: no automated test suite yet (planned for v1.0), and WebAuthn/email not yet battle-tested across many environments. Suitable for your own apps; harden before betting a production product on it.


More

License

MIT — see LICENSE.

Product 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 was computed.  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 was computed.  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
0.8.0 0 6/10/2026
0.7.3 0 6/10/2026
0.7.2 31 6/9/2026
0.7.1 33 6/9/2026
0.7.0 51 6/8/2026
0.6.1 55 6/8/2026