ASP.NET Core 3.1 - 2FA User Tokens


Ken Haggerty
Created 08/20/2020 - Updated 08/31/2020 06:28

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.

Users Without Identity Project and Article Series

Access to the research project source code may be purchased on KenHaggerty.Com at Manage > Assets. A project which implements users without Identity has been published to demo.kenhaggerty.com. I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

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(
            name: "TwoFactorEnabled",
            table: "AppUsers",
            nullable: false,
            defaultValue: false);

        migrationBuilder.CreateTable(
            name: "AppUserTokens",
            columns: table => new
            {
                AppUserId = table.Column(nullable: false),
                Name = table.Column(maxLength: 128, nullable: false),
                Value = table.Column(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 08/28/2020

I updated the series article links.

Update 08/31/2020

I added the 2FA Admin Override article link.

Article Tags:

2FA Authorization EF Core Model

Comment Count = 0

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications