ASP.NET Core 3.1 - 2FA User Tokens
This 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)