LowCodeHub.OTP 0.0.5

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

LowCodeHub.OTP

A provider-neutral OTP flow library for .NET. It gives applications a standard SendAsync, VerifyAsync, and ResendAsync workflow with reference ids, hashed OTP storage, expiry, resend cooldowns, max resend limits, max verification attempts, and replay prevention.

NuGet License: MIT

Why This Library?

Feature LowCodeHub.OTP
Standard flow SendAsync, VerifyAsync, ResendAsync
Reference ids Every OTP challenge has a ReferenceId
Secure storage Stores salted hashes, never plaintext codes
Replay prevention Successful verification consumes the challenge
Resend rules Cooldown + maximum resend count
Brute-force guard Maximum failed verification attempts
Delivery Provider-neutral IOtpSender abstraction
Stores In-memory, Redis, SQL Server, PostgreSQL

Installation

dotnet add package LowCodeHub.OTP

Quick Start

using LowCodeHub.OTP.Abstractions;
using LowCodeHub.OTP.Extensions;
using LowCodeHub.OTP.Models;

builder.Services
    .AddOtp(options =>
    {
        options.CodeLength = 6;
        options.Expiry = TimeSpan.FromMinutes(5);
        options.ResendCooldown = TimeSpan.FromSeconds(60);
        options.MaxResendCount = 3;
        options.MaxVerifyAttempts = 5;
        options.HashPepper = builder.Configuration["Otp:HashPepper"] ?? string.Empty;
    })
    .AddOtpRedis(redis =>
    {
        redis.ConnectionString = builder.Configuration.GetConnectionString("Redis")!;
    });

builder.Services.AddScoped<IOtpSender, SmsOtpSender>();
public sealed class SmsOtpSender : IOtpSender
{
    public Task SendAsync(OtpDeliveryMessage message, CancellationToken cancellationToken = default)
    {
        // Send message.Code through your SMS/email/WhatsApp provider.
        return Task.CompletedTask;
    }
}

Send, Verify, Resend

app.MapPost("/otp/send", async (IOtpService otp, CancellationToken ct) =>
{
    OtpSendResult result = await otp.SendAsync(new OtpSendRequest
    {
        Recipient = "+15551234567",
        Channel = "sms",
        Purpose = "login"
    }, ct);

    return TypedResults.Ok(result);
});

app.MapPost("/otp/verify", async (IOtpService otp, OtpVerifyRequest request, CancellationToken ct) =>
{
    OtpVerifyResult result = await otp.VerifyAsync(request, ct);
    return result.Succeeded ? TypedResults.NoContent() : TypedResults.BadRequest(result);
});

app.MapPost("/otp/resend", async (IOtpService otp, OtpResendRequest request, CancellationToken ct) =>
{
    OtpResendResult result = await otp.ResendAsync(request, ct);
    return result.Succeeded ? TypedResults.Ok(result) : TypedResults.BadRequest(result);
});

Configuration

{
  "Otp": {
    "CodeLength": 6,
    "Expiry": "00:05:00",
    "ResendCooldown": "00:01:00",
    "MaxResendCount": 3,
    "MaxVerifyAttempts": 5,
    "ReferenceIdPrefix": "otp_",
    "HashPepper": "load-this-from-a-secret-store",
    "MockCode": null,
    "UseMockSender": false
  },
  "Otp:Redis": {
    "ConnectionString": "localhost:6379",
    "KeyPrefix": "otp:",
    "ExpiredRecordRetention": "01:00:00"
  }
}

Storage Providers

In-memory is registered by default and is useful for development and tests:

builder.Services.AddOtp();

This stores OTP records in process memory only. It does not create, migrate, or write to any database.

Redis:

builder.Services
    .AddOtp()
    .AddOtpRedis(options => options.ConnectionString = "localhost:6379");

This stores OTP records in Redis only. It does not insert records into SQL Server or PostgreSQL, and it does not require the SQL migration scripts.

SQL Server:

builder.Services
    .AddOtp()
    .AddOtpSqlServer(options =>
    {
        options.ConnectionString = builder.Configuration.GetConnectionString("SqlServer")!;
        options.Schema = "dbo";
        options.Table = "OtpRecords";
    });

PostgreSQL:

builder.Services
    .AddOtp()
    .AddOtpPostgreSql(options =>
    {
        options.ConnectionString = builder.Configuration.GetConnectionString("PostgreSql")!;
        options.Schema = "public";
        options.Table = "otp_records";
    });

SQL migration scripts are embedded under:

  • Repositories/SqlServer/Scripts
  • Repositories/PostgreSql/Scripts

Run the matching script in your database before using a SQL-backed store.

Initialize the Database

LowCodeHub.OTP does not create or migrate its own database schema during service registration. The schema scripts are idempotent and ship as embedded resources in the package, but the consuming application owns when and how they are applied.

You only need this section when using .AddOtpSqlServer(...) or .AddOtpPostgreSql(...). If you use the default in-memory store or .AddOtpRedis(...), no SQL table is used and no database migration is required.

Embedded resource prefixes:

Provider Embedded resource prefix
SQL Server LowCodeHub.OTP.Repositories.SqlServer.Scripts.
PostgreSQL LowCodeHub.OTP.Repositories.PostgreSql.Scripts.

The scripts create the OTP records table and indexes used by the SQL Server and PostgreSQL stores.

With LowCodeHub.Migration.SqlServer

Configure the migration package to scan the LowCodeHub.OTP assembly and include only the SQL Server OTP scripts:

using LowCodeHub.Migration.SqlServer.Extensions;
using LowCodeHub.OTP.Options;

builder.Services.AddMigration(o =>
{
    o.ConnectionString = builder.Configuration.GetConnectionString("Otp")!;
    o.Directories = ["LowCodeHub.OTP.Repositories.SqlServer.Scripts."];
});

await app.RunDatabaseMigrationAsync<SqlServerOtpOptions>();

With LowCodeHub.Migration.PostgreSql

using LowCodeHub.Migration.PostgreSql.Extensions;
using LowCodeHub.OTP.Options;

builder.Services.AddMigration(o =>
{
    o.ConnectionString = builder.Configuration.GetConnectionString("Otp")!;
    o.Directories = ["LowCodeHub.OTP.Repositories.PostgreSql.Scripts."];
});

await app.RunDatabaseMigrationAsync<PostgreSqlOtpOptions>();

Do not scan the whole LowCodeHub.OTP assembly without a directory filter, because the package contains scripts for both providers.

With EF Core Migrations

EF Core will not discover these embedded scripts automatically. They are not EF model migrations and they will not appear in dotnet ef migrations list. EF users should create an application-owned EF migration and execute the embedded scripts from the LowCodeHub.OTP assembly in Up.

Example SQL Server migration:

using LowCodeHub.OTP.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;

public partial class AddLowCodeHubOtpSchema : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        foreach (var resource in SqlOtp.SqlServerResources)
        {
            migrationBuilder.Sql(SqlOtp.ReadFromResource(resource));
        }
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // Drop OTP objects here if your application's migration policy requires reversible migrations.
    }
}

For PostgreSQL, loop over SqlOtp.PostgreSqlResources instead. SqlOtp.ReadFromResource(...) removes SQL Server GO separator lines so the SQL can be passed directly to EF Core's migrationBuilder.Sql(...).

Delivery Boundary

LowCodeHub.OTP does not depend on Twilio, SendGrid, SMTP, WhatsApp, or any other delivery provider. Applications own delivery by implementing IOtpSender.

This keeps the package generic, testable, and safe for different product requirements.

Mock Mode

For local development or automated tests, you can force a fixed code and use a no-op sender before a real IOtpSender exists:

builder.Services.AddOtp(options =>
{
    options.CodeLength = 4;
    options.MockCode = "1234";
    options.UseMockSender = true;
});

MockCode must be numeric and its length must match CodeLength. With the example above, VerifyAsync accepts 1234.

Security Notes

  • Plaintext OTP codes are only passed to IOtpSender; they are not stored.
  • Stored codes are salted and hashed.
  • Set Otp:HashPepper from a secret store in production.
  • Verification uses constant-time hash comparison.
  • A successful verification consumes the challenge and prevents replay.
  • Resend rotates the OTP code and resets failed verification attempts.
  • Keep OTP messages short and never log OtpDeliveryMessage.Code.

Otp.NET

LowCodeHub.OTP uses Otp.NET internally for HOTP numeric code generation primitives, then adds the application flow Otp.NET intentionally does not own: persistence, resend policy, expiry, and one-time-use enforcement.

Product Compatible and additional computed target framework versions.
.NET 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. 
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.0.5 102 5/18/2026
0.0.4 97 5/13/2026
0.0.3 93 5/12/2026
0.0.2 89 5/12/2026
0.0.1 93 5/12/2026