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)