LowCodeHub.OTP
0.0.5
dotnet add package LowCodeHub.OTP --version 0.0.5
NuGet\Install-Package LowCodeHub.OTP -Version 0.0.5
<PackageReference Include="LowCodeHub.OTP" Version="0.0.5" />
<PackageVersion Include="LowCodeHub.OTP" Version="0.0.5" />
<PackageReference Include="LowCodeHub.OTP" />
paket add LowCodeHub.OTP --version 0.0.5
#r "nuget: LowCodeHub.OTP, 0.0.5"
#:package LowCodeHub.OTP@0.0.5
#addin nuget:?package=LowCodeHub.OTP&version=0.0.5
#tool nuget:?package=LowCodeHub.OTP&version=0.0.5
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.
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/ScriptsRepositories/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:HashPepperfrom 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 | Versions 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. |
-
net10.0
- Microsoft.Data.SqlClient (>= 7.0.1)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.8)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.8)
- Npgsql (>= 10.0.2)
- Otp.NET (>= 1.4.1)
- StackExchange.Redis (>= 2.13.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.