DGates.Identity.Jwt2Fa
1.1.2
dotnet add package DGates.Identity.Jwt2Fa --version 1.1.2
NuGet\Install-Package DGates.Identity.Jwt2Fa -Version 1.1.2
<PackageReference Include="DGates.Identity.Jwt2Fa" Version="1.1.2" />
<PackageVersion Include="DGates.Identity.Jwt2Fa" Version="1.1.2" />
<PackageReference Include="DGates.Identity.Jwt2Fa" />
paket add DGates.Identity.Jwt2Fa --version 1.1.2
#r "nuget: DGates.Identity.Jwt2Fa, 1.1.2"
#:package DGates.Identity.Jwt2Fa@1.1.2
#addin nuget:?package=DGates.Identity.Jwt2Fa&version=1.1.2
#tool nuget:?package=DGates.Identity.Jwt2Fa&version=1.1.2
DGates.Identity.Jwt2Fa
Real, claims-bearing JWTs and multi-channel two-factor authentication (Authenticator/TOTP, Email, SMS) for ASP.NET Core Identity — generic over your own user type.
Targets .NET 10 only — not compatible with .NET Framework (e.g. net48).
See it running
angular-dotnet-auth-template is a full Angular + .NET app built on this package, with a live demo. Register an account and try authenticator/TOTP, email, and SMS 2FA. No real email or SMS is sent — messages land in the public mock inboxes.
- Live demo
- SendGrid mock (email inbox)
- Twilio mock (SMS inbox)
What you get
- Real, signed JWTs with a projected user claim and role claims — not opaque, Data-Protection-encrypted tokens
register/login/secureand the full password and email-confirmation lifecycle- Admin user management: list, get, create, update, unlock
- Multi-channel 2FA: authenticator app (TOTP), email codes, SMS codes
- Generic over your own
TUser : IdentityUser, with opt-in capability interfaces — no forced base class beyond Identity's own - An optional activation gate (
loginrejects inactive users) - Every endpoint gets a generic-500 safety net and DTO validation automatically
Why this instead of MapIdentityApi<TUser>?
ASP.NET Core's built-in AddIdentityApiEndpoints<TUser>()/MapIdentityApi<TUser>()
(since .NET 8) already covers "Identity as a JSON API" — that's not the pitch here.
Two things it doesn't cover:
- Multi-channel 2FA. Its two-factor support is TOTP-authenticator +
recovery-codes only — no SMS or email OTP delivery. This package adds both, using
the same
IUserTwoFactorTokenProvider<TUser>mechanism Identity already has, not a bolted-on parallel system. - Real JWTs. Its bearer tokens are opaque, Data-Protection-encrypted tokens — no readable claims, no interop with other JWT-consuming systems. This package issues standard signed JWTs with your own projected user claim embedded.
Duende IdentityServer is the other "real JWTs + Identity" option, but it solves a different, larger problem (a full OAuth2/OIDC provider for third-party clients) and requires a paid license above a small revenue threshold. If you need that, use it — this package is for a simpler case: your own first-party client(s) authenticating directly against your own API.
Install
dotnet add package DGates.Identity.Jwt2Fa
DGates.Identity.NotificationProviders
is a real, shipped dependency — installing this package already brings it in, you
don't add it separately. It supplies the ISmsSender interface and concrete senders,
but none are wired up automatically: registration and 2FA still need an
IEmailSender (Microsoft.AspNetCore.Identity.UI.Services.IEmailSender) and, for
SMS 2FA, an ISmsSender explicitly registered:
builder.Services.AddSmtpEmailSender(builder.Configuration);
// or: AddSendGridEmailSender / AddPostMarkEmailSender
builder.Services.AddTwilioSmsSender(builder.Configuration);
// or: AddSnsSmsSender
Register only one provider per channel — the last one registered wins for its interface. Or bring your own implementation of either interface instead.
Usage
Minimal setup
Everything beyond bare IdentityUser is opt-in — this gets you register, login,
secure, and the whole password/email lifecycle, with no custom user type at all:
builder.Services
.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<YourDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthCore<IdentityUser>(builder.Configuration, user => new
{
user.Id,
user.Email
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAuthCore<IdentityUser>();
app.Run();
Full setup
Two-factor auth, an activation gate, admin-created accounts, and role membership in
responses each need TUser to implement one small interface — nothing you don't use:
public class AppUser : IdentityUser, IActivatableUser, IAdminProvisionableUser, IMultiFactorMethodUser, IRoleAwareUser
{
public bool IsActive { get; set; } = true;
public bool HasSetPassword { get; set; }
public string? TwoFactorMethod { get; set; }
public List<string> Roles { get; set; } = new();
}
builder.Services
.AddIdentity<AppUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthCore<AppUser>(builder.Configuration, user => new
{
user.Id,
user.Email,
user.TwoFactorEnabled,
user.Roles
});
builder.Services.AddDefaultActivationPolicy<AppUser>();
builder.Services.Add2Fa<AppUser>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAuthCore<AppUser>();
app.Map2Fa<AppUser>();
app.MapDefaultActivationPolicy<AppUser>();
app.Run();
Every MapXyz call takes an optional route prefix (default "/auth"), so both
modules land under one route tree. The AddAuthCore projector delegate
(Func<TUser, object>) controls what's embedded in the JWT's "user" claim and
returned from auth responses — project down to what's safe to hand the client,
never the raw TUser. Every issued JWT also carries a standard ClaimTypes.Role
claim per role, so [Authorize(Roles = "YourRole")] works out of the box on your
own endpoints too.
A login, concretely
POST /auth/login for a user in the Admin role, with the Full setup's projector
above. Real output, fake data — email/password in, a 200 with a user object and
a real signed JWT out:
{
"isAuthSuccessful": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "8ed87255-2dc6-478e-a413-57cf70e3a769",
"email": "jane.doe@example.com",
"twoFactorEnabled": false,
"roles": ["Admin"]
}
}
The token's decoded payload — readable claims, not an opaque reference:
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "8ed87255-2dc6-478e-a413-57cf70e3a769",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "jane.doe@example.com",
"user": { "Id": "8ed87255...", "Email": "jane.doe@example.com", "TwoFactorEnabled": false, "Roles": ["Admin"] },
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
"exp": 1789992073,
"iss": "your-app",
"aud": "your-app-client"
}
The "user" claim is whatever the projector returns; the separate role claim is
what [Authorize(Roles = "Admin")] actually checks, issued independently of it.
Configuration
{
"Jwt2FaConfig": {
"SecurityKey": "at-least-32-bytes-of-random-secret-here",
"ValidIssuer": "your-app",
"ValidAudience": "your-app-client",
"ExpiryInMinutes": 60,
"AdminRoleName": "Admin"
},
"Jwt2FaAuthCoreConfig": {
"ApplicationName": "Your App",
"FrontendBaseUrl": "https://your-app.example.com",
"EmailConfirmationPath": "/email-confirmation?userId={userId}&code={code}",
"ForgotPasswordPath": "/forgot-password/reset?userId={userId}&code={code}"
}
}
FrontendBaseUrl (no trailing slash) is combined with the two path templates to
build the links emailed to users — only the domain is configured once. See
Configuration
for AdminRoleName, MaxPageSize, and overriding email/SMS copy.
Design: modules & capabilities
Two opt-in modules (AddAuthCore/MapAuthCore and Add2Fa/Map2Fa). The 2FA
module requires IMultiFactorMethodUser on your TUser; three other capability
interfaces (activation gate, admin-provisioned accounts, role-aware lookups)
optionally enhance core, with no loss of functionality if you skip them. Skip a
module, never see its interface; opt into a module without its interface, and it's
a compile error, not a silent no-op.
See Design for the full module table, admin endpoint behavior, and the three capability interfaces.
Part of a small ecosystem
| Project | What it is | Reach for it when |
|---|---|---|
| DGates.Identity.Jwt2Fa (you are here) | JWT issuance and multi-channel 2FA for ASP.NET Core Identity | you want real JWTs and TOTP/email/SMS 2FA on your own API |
| DGates.Identity.NotificationProviders (NuGet) | Email and SMS senders (SendGrid, SMTP, Postmark, Twilio, SNS) | you need swappable notification providers |
| angular-dotnet-auth-template | Angular 21 + .NET 10 starter with this package wired in, live demo, Cloud Run pipeline | you want a running app, not just the library |
| dgates-mock-servers | Public GHCR images mocking SendGrid, Twilio, and Postmark | you want to develop or test notification flows with no accounts |
angular-dotnet-auth-template → DGates.Identity.Jwt2Fa → DGates.Identity.NotificationProviders → dgates-mock-servers (in dev)
More from dgates82: DGates.AwsSecretsManager and dotnet-nuget-release-template, the template this package was scaffolded from.
License
MIT — see LICENSE.
| 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
- DGates.Identity.NotificationProviders (>= 1.1.1)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.10)
- Microsoft.AspNetCore.Identity.UI (>= 10.0.10)
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 |
|---|---|---|
| 1.1.2 | 361 | 9/22/2026 |
| 1.1.2-RC.1 | 73 | 9/22/2026 |
| 1.1.1 | 99 | 9/21/2026 |
| 1.1.1-RC.1 | 66 | 9/21/2026 |
| 1.1.0 | 298 | 9/17/2026 |
| 1.1.0-RC.1 | 77 | 9/17/2026 |
| 1.0.0 | 433 | 8/22/2026 |
| 1.0.0-beta.8 | 95 | 8/21/2026 |
| 1.0.0-beta.7 | 121 | 8/20/2026 |
| 1.0.0-beta.6 | 98 | 8/20/2026 |
| 1.0.0-beta.5 | 87 | 8/18/2026 |
| 1.0.0-beta.4 | 190 | 8/7/2026 |
| 1.0.0-beta.3 | 82 | 8/7/2026 |
| 1.0.0-beta.2 | 74 | 8/7/2026 |
| 1.0.0-beta.1 | 84 | 8/7/2026 |