ASP.NET Core 3.1 - 2FA User Tokens

Ken Haggerty
Created 08/20/2020 - Updated 02/24/2021 00:02

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

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.

Article Tags:

2FA Authorization EF Core Model

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications