ASP.NET Core 3.1 - 2FA User Tokens
Download KH Authenticator

.NET MAUI App for Windows and Android
Online Registration and Authentication
No Password Or Email Address Required!
Certified Providers
KenHaggerty.Com Users Without Passwords Users With Passwords Users Without IdentityThis article will describe the implementation of a TwoFactorEnabled property for the user and a related AppUserToken entity/table. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.
Users Without Identity Project and Article Series
Creation Series
- ASP.NET Core 3.1 - Users Without Identity
- ASP.NET Core 3.1 - User Entity
- ASP.NET Core 3.1 - Password Hasher
- ASP.NET Core 3.1 - User Management
- ASP.NET Core 3.1 - Admin Role
- ASP.NET Core 3.1 - Cookie Validator
- ASP.NET Core 3.1 - Concurrency Conflicts
- ASP.NET Core 3.1 - Must Change Password
- ASP.NET Core 3.1 - User Database Service
- ASP.NET Core 3.1 - Rename Related Entities
2FA Series
- ASP.NET Core 3.1 - 2FA Without Identity
- ASP.NET Core 3.1 - 2FA User Tokens
- ASP.NET Core 3.1 - 2FA Cookie Schemes
- ASP.NET Core 3.1 - 2FA Authenticating
- ASP.NET Core 3.1 - 2FA Sign In Service
- ASP.NET Core 3.1 - 2FA QR Code Generator
- ASP.NET Core 3.1 - Admin 2FA Requirement
- ASP.NET Core 3.1 - 2FA Admin Override
Enhanced User Series
- ASP.NET Core 3.1 - Enhanced User Without Identity
- ASP.NET Core 3.1 - 2FA Recovery Codes
- ASP.NET Core 3.1 - Login Lockout
- ASP.NET Core 3.1 - Created And Last Login Date
- ASP.NET Core 3.1 - Security Stamp
- ASP.NET Core 3.1 - Token Service
- ASP.NET Core 3.1 - Confirmed Email Address
- ASP.NET Core 3.1 - Password Recovery
The Users Without Identity Project (UWIP) implements AppUser, a minimal entity which is created by administrators who invite the user to login. To implement 2FA, we need a TwoFactorEnabled property for the user and a related AppUserToken entity.
Add the TwoFactorEnabled property and the virtual AppUserTokens navigational property to AppUser.
Edit Entities > AppUser.cs:
public class AppUser { [Key] public int Id { get; set; } [Required] [Display(Name = "Login Name")] [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] public string LoginName { get; set; } [Required] [StringLength(32)] public string LoginNameUppercase { get; set; } [Required] [MaxLength(84, ErrorMessage = "The {0} is max {1} characters long.")] public string PasswordHash { get; set; } [Display(Name = "MCP")] public bool MustChangePassword { get; set; } [Display(Name = "2FA")] public bool TwoFactorEnabled { get; set; } [Display(Name = "Admin")] public bool IsAdmin { get; set; } [Timestamp] public byte[] RowVersion { get; set; } public virtual ICollection<AppUserToken> AppUserTokens { get; set; } }
Add the AppUserToken entity with the virtual AppUser navigational property.
Entities > AppUserToken.cs:
public class AppUserToken { [Required] public int AppUserId { get; set; } [Required] [StringLength(128)] public string Name { get; set; } public string Value { get; set; } public virtual AppUser AppUser { get; set; } }
Add the AppUserTokens property to ApplicationDbContext and the composite primary key to the OnModelCreating method.
Edit Data > ApplicationDbContext.cs:
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<AppUser> AppUsers { get; set; } public DbSet<AppUserToken> AppUserTokens { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Set a unique constraint for LoginNameUppercase builder.Entity<AppUser>() .HasIndex(u => u.LoginNameUppercase) .IsUnique(); // Composite primary key builder.Entity<AppUserToken>() .HasKey(t => new { t.AppUserId, t.Name }) .IsClustered(false); } }
Use the Package Manager Console to add an InitialAppUserTokens migration and update the database. The navigational properties will create a ForeignKey with a cascade delete rule.
public partial class InitialAppUserTokens : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<bool>( name: "TwoFactorEnabled", table: "AppUsers", nullable: false, defaultValue: false); migrationBuilder.CreateTable( name: "AppUserTokens", columns: table => new { AppUserId = table.Column<int>(nullable: false), Name = table.Column<string>(maxLength: 128, nullable: false), Value = table.Column<string>(nullable: true) }, constraints: table => { table.PrimaryKey("PK_AppUserTokens", x => new { x.AppUserId, x.Name }) .Annotation("SqlServer:Clustered", false); table.ForeignKey( name: "FK_AppUserTokens_AppUsers_AppUserId", column: x => x.AppUserId, principalTable: "AppUsers", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "AppUserTokens"); migrationBuilder.DropColumn( name: "TwoFactorEnabled", table: "AppUsers"); } }
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)