ASP.NET Core 3.1 - 2FA User Tokens

Ken Haggerty
Created 08/20/2020 - Updated 11/25/2020 00:22

This article will describe the implementation of a TwoFactorEnabled property for the user and a related AppUserToken entity/table. I will assume you have 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.

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 11/24/2020

I added the Rename Related Entities article and updated the project and article links. Updated code formatting.

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