LowCodeHub.OTP
0.0.11
dotnet add package LowCodeHub.OTP --version 0.0.11
NuGet\Install-Package LowCodeHub.OTP -Version 0.0.11
<PackageReference Include="LowCodeHub.OTP" Version="0.0.11" />
<PackageVersion Include="LowCodeHub.OTP" Version="0.0.11" />
<PackageReference Include="LowCodeHub.OTP" />
paket add LowCodeHub.OTP --version 0.0.11
#r "nuget: LowCodeHub.OTP, 0.0.11"
#:package LowCodeHub.OTP@0.0.11
#addin nuget:?package=LowCodeHub.OTP&version=0.0.11
#tool nuget:?package=LowCodeHub.OTP&version=0.0.11
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,
"ExpiredRecordRetention": "01:00:00",
"CleanupInterval": "00:15:00"
},
"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.Migrations;
builder.Services.AddMigration(o =>
{
o.ConnectionString = builder.Configuration.GetConnectionString("Otp")!;
o.Directories = ["LowCodeHub.OTP.Repositories.SqlServer.Scripts."];
});
await app.RunDatabaseMigrationAsync<IOtpScriptScanner>();
With LowCodeHub.Migration.PostgreSql
using LowCodeHub.Migration.PostgreSql.Extensions;
using LowCodeHub.OTP.Migrations;
builder.Services.AddMigration(o =>
{
o.ConnectionString = builder.Configuration.GetConnectionString("Otp")!;
o.Directories = ["LowCodeHub.OTP.Repositories.PostgreSql.Scripts."];
});
await app.RunDatabaseMigrationAsync<IOtpScriptScanner>();
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.
When MockCode or UseMockSender is set, a warning is logged at application startup so a leftover mock configuration cannot reach production silently.
Expired Record Cleanup
A background service periodically deletes expired OTP records once they have passed Otp:ExpiredRecordRetention (so callers still receive an Expired result instead of NotFound shortly after expiry). It runs every Otp:CleanupInterval (default 15 minutes); set the interval to 00:00:00 to disable it and manage cleanup yourself. The Redis store sets native key TTLs (Otp:Redis:ExpiredRecordRetention) instead, so its cleanup pass is a no-op.
Security Notes
- Codes are generated with a cryptographically secure RNG and are uniformly distributed.
- Plaintext OTP codes are only passed to
IOtpSender; they are not stored. - Stored codes are salted and hashed (PBKDF2 with a server-side pepper).
- Set
Otp:HashPepperfrom a secret store in production. Rotating the pepper invalidates all outstanding codes — they fail verification asInvalidCode. - Verification uses constant-time hash comparison.
- A successful verification consumes the challenge and prevents replay.
- Resend rotates the OTP code, extends expiry, and resets failed verification attempts, so the total guess budget for one reference id is
MaxVerifyAttempts × (MaxResendCount + 1)(20 with default settings). Tune both limits together. - Recipients are trimmed and lower-cased before storage and cooldown matching, so
User@example.comanduser@example.comare the same recipient on every store. - The resend cooldown is a best-effort guard, not a hard rate limit: the check is read-then-write, so two perfectly concurrent
SendAsynccalls for the same recipient can both pass it. Put a real rate limiter in front of public OTP endpoints. - If
IOtpSenderthrows duringSendAsync, the just-created record is deleted so the recipient is not locked out of requesting a new code by the cooldown. - Keep OTP messages short and never log
OtpDeliveryMessage.Code.
| 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.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.9)
- Npgsql (>= 10.0.3)
- StackExchange.Redis (>= 3.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.