Add authenticator support
This commit is contained in:
parent
a9ce48b911
commit
5aed9742a4
|
|
@ -377,6 +377,11 @@ namespace IdentitySample.Controllers
|
||||||
return View("Error");
|
return View("Error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.SelectedProvider == "Authenticator")
|
||||||
|
{
|
||||||
|
return RedirectToAction(nameof(VerifyAuthenticatorCode), new { ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
|
||||||
|
}
|
||||||
|
|
||||||
// Generate the token and send it
|
// Generate the token and send it
|
||||||
var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider);
|
var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider);
|
||||||
if (string.IsNullOrWhiteSpace(code))
|
if (string.IsNullOrWhiteSpace(code))
|
||||||
|
|
@ -444,6 +449,93 @@ namespace IdentitySample.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// GET: /Account/VerifyAuthenticatorCode
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> VerifyAuthenticatorCode(bool rememberMe, string returnUrl = null)
|
||||||
|
{
|
||||||
|
// Require that the user has already logged in via username/password or external login
|
||||||
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return View("Error");
|
||||||
|
}
|
||||||
|
return View(new VerifyAuthenticatorCodeViewModel { ReturnUrl = returnUrl, RememberMe = rememberMe });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// POST: /Account/VerifyAuthenticatorCode
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> VerifyAuthenticatorCode(VerifyAuthenticatorCodeViewModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following code protects for brute force attacks against the two factor codes.
|
||||||
|
// If a user enters incorrect codes for a specified amount of time then the user account
|
||||||
|
// will be locked out for a specified amount of time.
|
||||||
|
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(model.Code, model.RememberMe, model.RememberBrowser);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return RedirectToLocal(model.ReturnUrl);
|
||||||
|
}
|
||||||
|
if (result.IsLockedOut)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(7, "User account locked out.");
|
||||||
|
return View("Lockout");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, "Invalid code.");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// GET: /Account/UseRecoveryCode
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> UseRecoveryCode(string returnUrl = null)
|
||||||
|
{
|
||||||
|
// Require that the user has already logged in via username/password or external login
|
||||||
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return View("Error");
|
||||||
|
}
|
||||||
|
return View(new UseRecoveryCodeViewModel { ReturnUrl = returnUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// POST: /Account/UseRecoveryCode
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> UseRecoveryCode(UseRecoveryCodeViewModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(model.Code);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return RedirectToLocal(model.ReturnUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, "Invalid code.");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
private void AddErrors(IdentityResult result)
|
private void AddErrors(IdentityResult result)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ namespace IdentitySamples.Controllers
|
||||||
PhoneNumber = await _userManager.GetPhoneNumberAsync(user),
|
PhoneNumber = await _userManager.GetPhoneNumberAsync(user),
|
||||||
TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user),
|
TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user),
|
||||||
Logins = await _userManager.GetLoginsAsync(user),
|
Logins = await _userManager.GetLoginsAsync(user),
|
||||||
BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user)
|
BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user),
|
||||||
|
AuthenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user)
|
||||||
};
|
};
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +108,37 @@ namespace IdentitySamples.Controllers
|
||||||
return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber });
|
return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// POST: /Manage/ResetAuthenticatorKey
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ResetAuthenticatorKey()
|
||||||
|
{
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||||
|
_logger.LogInformation(1, "User reset authenticator key.");
|
||||||
|
}
|
||||||
|
return RedirectToAction(nameof(Index), "Manage");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// POST: /Manage/GenerateRecoveryCode
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> GenerateRecoveryCode()
|
||||||
|
{
|
||||||
|
var user = await GetCurrentUserAsync();
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var codes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 5);
|
||||||
|
_logger.LogInformation(1, "User generated new recovery code.");
|
||||||
|
return View("DisplayRecoveryCodes", new DisplayRecoveryCodesViewModel { Codes = codes });
|
||||||
|
}
|
||||||
|
return View("Error");
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// POST: /Manage/EnableTwoFactorAuthentication
|
// POST: /Manage/EnableTwoFactorAuthentication
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IdentitySample.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using IdentitySample.Models;
|
|
||||||
|
|
||||||
namespace IdentitySample.Data.Migrations
|
namespace IdentitySample.Data.Migrations
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +17,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "7.0.0-rc2")
|
.HasAnnotation("ProductVersion", "1.0.0-rc3")
|
||||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b =>
|
||||||
|
|
@ -36,9 +36,9 @@ namespace IdentitySample.Data.Migrations
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
b.HasIndex("NormalizedName")
|
||||||
.HasAnnotation("Relational:Name", "RoleNameIndex");
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetRoles");
|
b.ToTable("AspNetRoles");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
||||||
|
|
@ -57,7 +57,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetRoleClaims");
|
b.ToTable("AspNetRoleClaims");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
||||||
|
|
@ -76,7 +76,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUserClaims");
|
b.ToTable("AspNetUserClaims");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
||||||
|
|
@ -94,7 +94,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUserLogins");
|
b.ToTable("AspNetUserLogins");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
||||||
|
|
@ -109,10 +109,25 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUserRoles");
|
b.ToTable("AspNetUserRoles");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("$safeprojectname$.Models.ApplicationUser", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("WebApplication13.Models.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id");
|
b.Property<string>("Id");
|
||||||
|
|
||||||
|
|
@ -152,34 +167,35 @@ namespace IdentitySample.Data.Migrations
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
b.HasIndex("NormalizedEmail")
|
||||||
.HasAnnotation("Relational:Name", "EmailIndex");
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
b.HasIndex("NormalizedUserName")
|
||||||
.HasAnnotation("Relational:Name", "UserNameIndex");
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
||||||
.WithMany()
|
.WithMany("Claims")
|
||||||
.HasForeignKey("RoleId")
|
.HasForeignKey("RoleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("$safeprojectname$.Models.ApplicationUser")
|
b.HasOne("WebApplication13.Models.ApplicationUser")
|
||||||
.WithMany()
|
.WithMany("Claims")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("$safeprojectname$.Models.ApplicationUser")
|
b.HasOne("WebApplication13.Models.ApplicationUser")
|
||||||
.WithMany()
|
.WithMany("Logins")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
@ -187,12 +203,12 @@ namespace IdentitySample.Data.Migrations
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
||||||
.WithMany()
|
.WithMany("Users")
|
||||||
.HasForeignKey("RoleId")
|
.HasForeignKey("RoleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
b.HasOne("$safeprojectname$.Models.ApplicationUser")
|
b.HasOne("WebApplication13.Models.ApplicationUser")
|
||||||
.WithMany()
|
.WithMany("Roles")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -17,14 +17,28 @@ namespace IdentitySample.Data.Migrations
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
ConcurrencyStamp = table.Column<string>(nullable: true),
|
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||||
Name = table.Column<string>(nullable: true),
|
Name = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
NormalizedName = table.Column<string>(nullable: true)
|
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserTokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(nullable: false),
|
||||||
|
LoginProvider = table.Column<string>(nullable: false),
|
||||||
|
Name = table.Column<string>(nullable: false),
|
||||||
|
Value = table.Column<string>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||||
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "AspNetUsers",
|
name: "AspNetUsers",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
|
|
@ -32,18 +46,18 @@ namespace IdentitySample.Data.Migrations
|
||||||
Id = table.Column<string>(nullable: false),
|
Id = table.Column<string>(nullable: false),
|
||||||
AccessFailedCount = table.Column<int>(nullable: false),
|
AccessFailedCount = table.Column<int>(nullable: false),
|
||||||
ConcurrencyStamp = table.Column<string>(nullable: true),
|
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||||
Email = table.Column<string>(nullable: true),
|
Email = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
EmailConfirmed = table.Column<bool>(nullable: false),
|
EmailConfirmed = table.Column<bool>(nullable: false),
|
||||||
LockoutEnabled = table.Column<bool>(nullable: false),
|
LockoutEnabled = table.Column<bool>(nullable: false),
|
||||||
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
||||||
NormalizedEmail = table.Column<string>(nullable: true),
|
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
NormalizedUserName = table.Column<string>(nullable: true),
|
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
PasswordHash = table.Column<string>(nullable: true),
|
PasswordHash = table.Column<string>(nullable: true),
|
||||||
PhoneNumber = table.Column<string>(nullable: true),
|
PhoneNumber = table.Column<string>(nullable: true),
|
||||||
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
||||||
SecurityStamp = table.Column<string>(nullable: true),
|
SecurityStamp = table.Column<string>(nullable: true),
|
||||||
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
||||||
UserName = table.Column<string>(nullable: true)
|
UserName = table.Column<string>(maxLength: 256, nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
@ -174,7 +188,8 @@ namespace IdentitySample.Data.Migrations
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "UserNameIndex",
|
name: "UserNameIndex",
|
||||||
table: "AspNetUsers",
|
table: "AspNetUsers",
|
||||||
column: "NormalizedUserName");
|
column: "NormalizedUserName",
|
||||||
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
|
@ -191,6 +206,9 @@ namespace IdentitySample.Data.Migrations
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "AspNetUserRoles");
|
name: "AspNetUserRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserTokens");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "AspNetRoles");
|
name: "AspNetRoles");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IdentitySample.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using IdentitySample.Models;
|
|
||||||
|
|
||||||
namespace IdentitySample.Data.Migrations
|
namespace IdentitySample.Data.Migrations
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +16,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "7.0.0-rc2")
|
.HasAnnotation("ProductVersion", "1.0.0-rc3")
|
||||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b =>
|
||||||
|
|
@ -35,9 +35,9 @@ namespace IdentitySample.Data.Migrations
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
b.HasIndex("NormalizedName")
|
||||||
.HasAnnotation("Relational:Name", "RoleNameIndex");
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetRoles");
|
b.ToTable("AspNetRoles");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
||||||
|
|
@ -56,7 +56,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetRoleClaims");
|
b.ToTable("AspNetRoleClaims");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
||||||
|
|
@ -75,7 +75,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUserClaims");
|
b.ToTable("AspNetUserClaims");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
||||||
|
|
@ -93,7 +93,7 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUserLogins");
|
b.ToTable("AspNetUserLogins");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
||||||
|
|
@ -108,10 +108,25 @@ namespace IdentitySample.Data.Migrations
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUserRoles");
|
b.ToTable("AspNetUserRoles");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("$safeprojectname$.Models.ApplicationUser", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentitySample.Models.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id");
|
b.Property<string>("Id");
|
||||||
|
|
||||||
|
|
@ -151,34 +166,35 @@ namespace IdentitySample.Data.Migrations
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
b.HasIndex("NormalizedEmail")
|
||||||
.HasAnnotation("Relational:Name", "EmailIndex");
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
b.HasIndex("NormalizedUserName")
|
||||||
.HasAnnotation("Relational:Name", "UserNameIndex");
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
b.HasAnnotation("Relational:TableName", "AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
||||||
.WithMany()
|
.WithMany("Claims")
|
||||||
.HasForeignKey("RoleId")
|
.HasForeignKey("RoleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("$safeprojectname$.Models.ApplicationUser")
|
b.HasOne("IdentitySample.Models.ApplicationUser")
|
||||||
.WithMany()
|
.WithMany("Claims")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("$safeprojectname$.Models.ApplicationUser")
|
b.HasOne("IdentitySample.Models.ApplicationUser")
|
||||||
.WithMany()
|
.WithMany("Logins")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
@ -186,12 +202,12 @@ namespace IdentitySample.Data.Migrations
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
|
||||||
.WithMany()
|
.WithMany("Users")
|
||||||
.HasForeignKey("RoleId")
|
.HasForeignKey("RoleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
b.HasOne("$safeprojectname$.Models.ApplicationUser")
|
b.HasOne("IdentitySample.Models.ApplicationUser")
|
||||||
.WithMany()
|
.WithMany("Roles")
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace IdentitySample.Models.AccountViewModels
|
||||||
|
{
|
||||||
|
public class UseRecoveryCodeViewModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Code { get; set; }
|
||||||
|
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace IdentitySample.Models.AccountViewModels
|
||||||
|
{
|
||||||
|
public class VerifyAuthenticatorCodeViewModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Code { get; set; }
|
||||||
|
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Remember this browser?")]
|
||||||
|
public bool RememberBrowser { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Remember me?")]
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
using System;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IdentitySample.Models.AccountViewModels
|
namespace IdentitySample.Models.AccountViewModels
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace IdentitySample.Models.ManageViewModels
|
||||||
|
{
|
||||||
|
public class DisplayRecoveryCodesViewModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public IEnumerable<string> Codes { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,5 +17,7 @@ namespace IdentitySample.Models.ManageViewModels
|
||||||
public bool TwoFactor { get; set; }
|
public bool TwoFactor { get; set; }
|
||||||
|
|
||||||
public bool BrowserRemembered { get; set; }
|
public bool BrowserRemembered { get; set; }
|
||||||
|
|
||||||
|
public string AuthenticatorKey { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace IdentitySample
|
namespace IdentitySample
|
||||||
{
|
{
|
||||||
|
|
@ -8,11 +7,8 @@ namespace IdentitySample
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
var config = new ConfigurationBuilder().AddEnvironmentVariables("ASPNETCORE_").Build();
|
|
||||||
|
|
||||||
var host = new WebHostBuilder()
|
var host = new WebHostBuilder()
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
.UseConfiguration(config)
|
|
||||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
.UseIISIntegration()
|
.UseIISIntegration()
|
||||||
.UseStartup<Startup>()
|
.UseStartup<Startup>()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNET_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,11 @@ namespace IdentitySample
|
||||||
{
|
{
|
||||||
public Startup(IHostingEnvironment env)
|
public Startup(IHostingEnvironment env)
|
||||||
{
|
{
|
||||||
// Set up configuration sources.
|
|
||||||
var builder = new ConfigurationBuilder()
|
var builder = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(env.ContentRootPath)
|
||||||
.AddJsonFile("appsettings.json")
|
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
|
||||||
{
|
|
||||||
// For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
|
|
||||||
builder.AddUserSecrets();
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AddEnvironmentVariables();
|
builder.AddEnvironmentVariables();
|
||||||
Configuration = builder.Build();
|
Configuration = builder.Build();
|
||||||
}
|
}
|
||||||
|
|
@ -44,13 +37,9 @@ namespace IdentitySample
|
||||||
{
|
{
|
||||||
// Add framework services.
|
// Add framework services.
|
||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
|
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
services.AddIdentity<ApplicationUser, IdentityRole>(options => {
|
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||||
options.Cookies.ApplicationCookie.AuthenticationScheme = "ApplicationCookie";
|
|
||||||
options.Cookies.ApplicationCookie.CookieName = "Interop";
|
|
||||||
options.Cookies.ApplicationCookie.DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo("C:\\Github\\Identity\\artifacts"));
|
|
||||||
})
|
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
.AddDefaultTokenProviders();
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
|
@ -75,19 +64,8 @@ namespace IdentitySample
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Home/Error");
|
app.UseExceptionHandler("/Home/Error");
|
||||||
|
|
||||||
// For more details on creating database during deployment see http://go.microsoft.com/fwlink/?LinkID=615859
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
|
|
||||||
.CreateScope())
|
|
||||||
{
|
|
||||||
serviceScope.ServiceProvider.GetService<ApplicationDbContext>()
|
|
||||||
.Database.Migrate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseIdentity();
|
app.UseIdentity();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
@model UseRecoveryCodeViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Use recovery code";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>@ViewData["Title"].</h2>
|
||||||
|
|
||||||
|
<form asp-controller="Account" asp-action="UseRecoveryCode" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<h4>@ViewData["Status"]</h4>
|
||||||
|
<hr />
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Code" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="Code" class="form-control" />
|
||||||
|
<span asp-validation-for="Code" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-offset-2 col-md-10">
|
||||||
|
<button type="submit" class="btn btn-default">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
@model VerifyAuthenticatorCodeViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Verify";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>@ViewData["Title"].</h2>
|
||||||
|
|
||||||
|
<form asp-controller="Account" asp-action="VerifyAuthenticatorCode" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<input asp-for="RememberMe" type="hidden" />
|
||||||
|
<h4>@ViewData["Status"]</h4>
|
||||||
|
<hr />
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Code" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="Code" class="form-control" />
|
||||||
|
<span asp-validation-for="Code" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-offset-2 col-md-10">
|
||||||
|
<div class="checkbox">
|
||||||
|
<input asp-for="RememberBrowser" />
|
||||||
|
<label asp-for="RememberBrowser"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-offset-2 col-md-10">
|
||||||
|
<button type="submit" class="btn btn-default">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<a asp-action="UseRecoveryCode">Lost your authenticator?</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
@model DisplayRecoveryCodesViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Your recovery codes:";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>@ViewData["Title"].</h2>
|
||||||
|
<p class="text-success">@ViewData["StatusMessage"]</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4>Here are your new recovery codes</h4>
|
||||||
|
<hr />
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Codes:</dt>
|
||||||
|
@foreach (var code in Model.Codes)
|
||||||
|
{
|
||||||
|
<dd>
|
||||||
|
<text>@code</text>
|
||||||
|
</dd>
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
See <a href="http://go.microsoft.com/fwlink/?LinkID=532713">this article</a>
|
See <a href="http://go.microsoft.com/fwlink/?LinkID=532713">this article</a>
|
||||||
for details on setting up this ASP.NET application to support two-factor authentication using SMS.
|
for details on setting up this ASP.NET application to support two-factor authentication using SMS.
|
||||||
</p>
|
</p>
|
||||||
@*@(Model.PhoneNumber ?? "None")
|
@(Model.PhoneNumber ?? "None")
|
||||||
@if (Model.PhoneNumber != null)
|
@if (Model.PhoneNumber != null)
|
||||||
{
|
{
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -44,16 +44,16 @@
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<text>[ <a asp-controller="Manage" asp-action="AddPhoneNumber">Add</a> ]</text>
|
<text>[ <a asp-controller="Manage" asp-action="AddPhoneNumber">Add</a> ]</text>
|
||||||
}*@
|
}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Two-Factor Authentication:</dt>
|
<dt>Two-Factor Authentication:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<p>
|
<!--<p>
|
||||||
There are no two-factor authentication providers configured. See <a href="http://go.microsoft.com/fwlink/?LinkID=532713">this article</a>
|
There are no two-factor authentication providers configured. See <a href="http://go.microsoft.com/fwlink/?LinkID=532713">this article</a>
|
||||||
for setting up this application to support two-factor authentication.
|
for setting up this application to support two-factor authentication.
|
||||||
</p>
|
</p>-->
|
||||||
@*@if (Model.TwoFactor)
|
@if (Model.TwoFactor)
|
||||||
{
|
{
|
||||||
<form asp-controller="Manage" asp-action="DisableTwoFactorAuthentication" method="post" class="form-horizontal" role="form">
|
<form asp-controller="Manage" asp-action="DisableTwoFactorAuthentication" method="post" class="form-horizontal" role="form">
|
||||||
Enabled [<button type="submit" class="btn-link">Disable</button>]
|
Enabled [<button type="submit" class="btn-link">Disable</button>]
|
||||||
|
|
@ -64,7 +64,23 @@
|
||||||
<form asp-controller="Manage" asp-action="EnableTwoFactorAuthentication" method="post" class="form-horizontal" role="form">
|
<form asp-controller="Manage" asp-action="EnableTwoFactorAuthentication" method="post" class="form-horizontal" role="form">
|
||||||
[<button type="submit" class="btn-link">Enable</button>] Disabled
|
[<button type="submit" class="btn-link">Enable</button>] Disabled
|
||||||
</form>
|
</form>
|
||||||
}*@
|
}
|
||||||
|
</dd>
|
||||||
|
<dt>Authentication App:</dt>
|
||||||
|
<dd>
|
||||||
|
@if (Model.AuthenticatorKey == null)
|
||||||
|
{
|
||||||
|
<form asp-controller="Manage" asp-action="ResetAuthenticatorKey" method="post" class="form-horizontal" role="form">
|
||||||
|
Generate [<button type="submit" class="btn-link">Generate</button>]
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<text>Your key is: @Model.AuthenticatorKey</text>
|
||||||
|
<form asp-controller="Manage" asp-action="GenerateRecoveryCode" method="post" class="form-horizontal" role="form">
|
||||||
|
Generate [<button type="submit" class="btn-link">Generate new recovery codes</button>]
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
@RenderBody()
|
@RenderBody()
|
||||||
<hr />
|
<hr />
|
||||||
<footer>
|
<footer>
|
||||||
<p>© $year$ - $safeprojectname$</p>
|
<p>© 2016 - IdentitySample</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"Data": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": {
|
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Interop-Shared-10-10;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||||
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Interop-Shared;Trusted_Connection=True;MultipleActiveResultSets=true"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"IncludeScopes": false,
|
"IncludeScopes": false,
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
IUserPhoneNumberStore<TUser>,
|
IUserPhoneNumberStore<TUser>,
|
||||||
IQueryableUserStore<TUser>,
|
IQueryableUserStore<TUser>,
|
||||||
IUserTwoFactorStore<TUser>,
|
IUserTwoFactorStore<TUser>,
|
||||||
IUserAuthenticationTokenStore<TUser>
|
IUserAuthenticationTokenStore<TUser>,
|
||||||
|
IUserAuthenticatorKeyStore<TUser>,
|
||||||
|
IUserTwoFactorRecoveryCodeStore<TUser>
|
||||||
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin>
|
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin>
|
||||||
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
|
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
|
||||||
where TContext : DbContext
|
where TContext : DbContext
|
||||||
|
|
@ -1411,7 +1413,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
/// <param name="name">The name of the token.</param>
|
/// <param name="name">The name of the token.</param>
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
|
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
|
||||||
public async virtual Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
public virtual async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
@ -1435,7 +1437,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
/// <param name="name">The name of the token.</param>
|
/// <param name="name">The name of the token.</param>
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
|
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
|
||||||
public async virtual Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
public virtual async Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
@ -1447,5 +1449,78 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
var entry = await FindToken(user, loginProvider, name, cancellationToken);
|
var entry = await FindToken(user, loginProvider, name, cancellationToken);
|
||||||
return entry?.Value;
|
return entry?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string InternalLoginProvider = "[AspNetUserStore]";
|
||||||
|
private const string AuthenticatorKeyTokenName = "AuthenticatorKey";
|
||||||
|
private const string RecoveryCodeTokenName = "RecoveryCodes";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the authenticator key for the specified <paramref name="user"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user whose authenticator key should be set.</param>
|
||||||
|
/// <param name="key">The authenticator key to set.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
|
||||||
|
public virtual Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the authenticator key for the specified <paramref name="user" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user whose security stamp should be set.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the security stamp for the specified <paramref name="user"/>.</returns>
|
||||||
|
public virtual Task<string> GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the recovery codes for the user while invalidating any previous recovery codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to store new recovery codes for.</param>
|
||||||
|
/// <param name="recoveryCodes">The new recovery codes for the user.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>The new recovery codes for the user.</returns>
|
||||||
|
public virtual Task ReplaceCodesAsync(TUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var mergedCodes = string.Join(";", recoveryCodes);
|
||||||
|
return SetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid
|
||||||
|
/// once, and will be invalid after use.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user who owns the recovery code.</param>
|
||||||
|
/// <param name="code">The recovery code to use.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>True if the recovery code was found for the user.</returns>
|
||||||
|
public virtual async Task<bool> RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
if (code == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? "";
|
||||||
|
var splitCodes = mergedCodes.Split(';');
|
||||||
|
if (splitCodes.Contains(code))
|
||||||
|
{
|
||||||
|
var updatedCodes = new List<string>(splitCodes.Where(s => s != code));
|
||||||
|
await ReplaceCodesAsync(user, updatedCodes, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Identity.Test
|
namespace Microsoft.AspNetCore.Identity.Test
|
||||||
|
|
@ -2371,6 +2373,69 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.Null(await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
|
Assert.Null(await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Task</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task CanRedeemRecoveryCodeOnlyOnce()
|
||||||
|
{
|
||||||
|
if (ShouldSkipDbTests())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var manager = CreateManager();
|
||||||
|
var user = CreateTestUser();
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
|
||||||
|
|
||||||
|
var numCodes = 15;
|
||||||
|
var newCodes = await manager.GenerateNewTwoFactorRecoveryCodesAsync(user, numCodes);
|
||||||
|
Assert.Equal(numCodes, newCodes.Count());
|
||||||
|
|
||||||
|
foreach (var code in newCodes)
|
||||||
|
{
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code));
|
||||||
|
IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code));
|
||||||
|
}
|
||||||
|
// One last time to be sure
|
||||||
|
foreach (var code in newCodes)
|
||||||
|
{
|
||||||
|
IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Task</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task RecoveryCodesInvalidAfterReplace()
|
||||||
|
{
|
||||||
|
if (ShouldSkipDbTests())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var manager = CreateManager();
|
||||||
|
var user = CreateTestUser();
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
|
||||||
|
|
||||||
|
var numCodes = 15;
|
||||||
|
var newCodes = await manager.GenerateNewTwoFactorRecoveryCodesAsync(user, numCodes);
|
||||||
|
Assert.Equal(numCodes, newCodes.Count());
|
||||||
|
var realCodes = await manager.GenerateNewTwoFactorRecoveryCodesAsync(user, numCodes);
|
||||||
|
Assert.Equal(numCodes, realCodes.Count());
|
||||||
|
|
||||||
|
foreach (var code in newCodes)
|
||||||
|
{
|
||||||
|
IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var code in realCodes)
|
||||||
|
{
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test.
|
/// Test.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -2408,6 +2473,11 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.NotNull(factors);
|
Assert.NotNull(factors);
|
||||||
Assert.Equal(1, factors.Count());
|
Assert.Equal(1, factors.Count());
|
||||||
Assert.Equal("Phone", factors[0]);
|
Assert.Equal("Phone", factors[0]);
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.ResetAuthenticatorKeyAsync(user));
|
||||||
|
factors = await manager.GetValidTwoFactorProvidersAsync(user);
|
||||||
|
Assert.NotNull(factors);
|
||||||
|
Assert.Equal(2, factors.Count());
|
||||||
|
Assert.Equal("Authenticator", factors[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used for authenticator code verification.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthenticatorTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUser> where TUser : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a two factor authentication token can be generated for the specified <paramref name="user"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve the <paramref name="user"/> from.</param>
|
||||||
|
/// <param name="user">The <typeparamref name="TUser"/> to check for the possibility of generating a two factor authentication token.</param>
|
||||||
|
/// <returns>True if the user has an authenticator key set, otherwise false.</returns>
|
||||||
|
public async virtual Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
|
||||||
|
{
|
||||||
|
var key = await manager.GetAuthenticatorKeyAsync(user);
|
||||||
|
return !string.IsNullOrWhiteSpace(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an empty string since no authenticator codes are sent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="purpose">Ignored.</param>
|
||||||
|
/// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve the <paramref name="user"/> from.</param>
|
||||||
|
/// <param name="user">The <typeparamref name="TUser"/>.</param>
|
||||||
|
/// <returns>string.Empty.</returns>
|
||||||
|
public virtual Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
|
||||||
|
{
|
||||||
|
return Task.FromResult(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="purpose"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <param name="manager"></param>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
|
||||||
|
{
|
||||||
|
var key = await manager.GetAuthenticatorKeyAsync(user);
|
||||||
|
int code;
|
||||||
|
if (!int.TryParse(token, out code))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = new HMACSHA1(Base32.FromBase32(key));
|
||||||
|
var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
|
||||||
|
var timestep = Convert.ToInt64(unixTimestamp / 30);
|
||||||
|
// Allow codes from 90s in each direction (we could make this configurable?)
|
||||||
|
for (int i = -2; i <= 2; i++)
|
||||||
|
{
|
||||||
|
var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
|
||||||
|
if (expectedCode == code)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
// See http://tools.ietf.org/html/rfc3548#section-5
|
||||||
|
internal static class Base32
|
||||||
|
{
|
||||||
|
private static readonly string _base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
|
||||||
|
public static string ToBase32(byte[] input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int offset = 0; offset < input.Length;)
|
||||||
|
{
|
||||||
|
byte a, b, c, d, e, f, g, h;
|
||||||
|
int numCharsToOutput = GetNextGroup(input, ref offset, out a, out b, out c, out d, out e, out f, out g, out h);
|
||||||
|
|
||||||
|
sb.Append((numCharsToOutput >= 1) ? _base32Chars[a] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 2) ? _base32Chars[b] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 3) ? _base32Chars[c] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 4) ? _base32Chars[d] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 5) ? _base32Chars[e] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 6) ? _base32Chars[f] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 7) ? _base32Chars[g] : '=');
|
||||||
|
sb.Append((numCharsToOutput >= 8) ? _base32Chars[h] : '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] FromBase32(string input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
input = input.TrimEnd('=').ToUpperInvariant();
|
||||||
|
if (input.Length == 0)
|
||||||
|
{
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = new byte[input.Length * 5 / 8];
|
||||||
|
var bitIndex = 0;
|
||||||
|
var inputIndex = 0;
|
||||||
|
var outputBits = 0;
|
||||||
|
var outputIndex = 0;
|
||||||
|
while (outputIndex < output.Length)
|
||||||
|
{
|
||||||
|
var byteIndex = _base32Chars.IndexOf(input[inputIndex]);
|
||||||
|
if (byteIndex < 0)
|
||||||
|
{
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = Math.Min(5 - bitIndex, 8 - outputBits);
|
||||||
|
output[outputIndex] <<= bits;
|
||||||
|
output[outputIndex] |= (byte)(byteIndex >> (5 - (bitIndex + bits)));
|
||||||
|
|
||||||
|
bitIndex += bits;
|
||||||
|
if (bitIndex >= 5)
|
||||||
|
{
|
||||||
|
inputIndex++;
|
||||||
|
bitIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputBits += bits;
|
||||||
|
if (outputBits >= 8)
|
||||||
|
{
|
||||||
|
outputIndex++;
|
||||||
|
outputBits = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the number of bytes that were output
|
||||||
|
private static int GetNextGroup(byte[] input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h)
|
||||||
|
{
|
||||||
|
uint b1, b2, b3, b4, b5;
|
||||||
|
|
||||||
|
int retVal;
|
||||||
|
switch (offset - input.Length)
|
||||||
|
{
|
||||||
|
case 1: retVal = 2; break;
|
||||||
|
case 2: retVal = 4; break;
|
||||||
|
case 3: retVal = 5; break;
|
||||||
|
case 4: retVal = 7; break;
|
||||||
|
default: retVal = 8; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
b1 = (offset < input.Length) ? input[offset++] : 0U;
|
||||||
|
b2 = (offset < input.Length) ? input[offset++] : 0U;
|
||||||
|
b3 = (offset < input.Length) ? input[offset++] : 0U;
|
||||||
|
b4 = (offset < input.Length) ? input[offset++] : 0U;
|
||||||
|
b5 = (offset < input.Length) ? input[offset++] : 0U;
|
||||||
|
|
||||||
|
a = (byte)(b1 >> 3);
|
||||||
|
b = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
|
||||||
|
c = (byte)((b2 >> 1) & 0x1f);
|
||||||
|
d = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
|
||||||
|
e = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
|
||||||
|
f = (byte)((b4 >> 2) & 0x1f);
|
||||||
|
g = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
|
||||||
|
h = (byte)(b5 & 0x1f);
|
||||||
|
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an abstraction for a store which stores info about user's authenticator.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
|
||||||
|
public interface IUserAuthenticatorKeyStore<TUser> : IUserStore<TUser> where TUser : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the authenticator key for the specified <paramref name="user"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user whose authenticator key should be set.</param>
|
||||||
|
/// <param name="key">The authenticator key to set.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
|
||||||
|
Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the authenticator key for the specified <paramref name="user" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user whose security stamp should be set.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the security stamp for the specified <paramref name="user"/>.</returns>
|
||||||
|
Task<string> GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an abstraction for a store which stores a user's recovery codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
|
||||||
|
public interface IUserTwoFactorRecoveryCodeStore<TUser> : IUserStore<TUser> where TUser : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the recovery codes for the user while invalidating any previous recovery codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to store new recovery codes for.</param>
|
||||||
|
/// <param name="recoveryCodes">The new recovery codes for the user.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>The new recovery codes for the user.</returns>
|
||||||
|
Task ReplaceCodesAsync(TUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid
|
||||||
|
/// once, and will be invalid after use.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user who owns the recovery code.</param>
|
||||||
|
/// <param name="code">The recovery code to use.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
|
||||||
|
/// <returns>True if the recovery code was found for the user.</returns>
|
||||||
|
Task<bool> RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -169,9 +169,11 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
var dataProtectionProviderType = typeof(DataProtectorTokenProvider<>).MakeGenericType(UserType);
|
var dataProtectionProviderType = typeof(DataProtectorTokenProvider<>).MakeGenericType(UserType);
|
||||||
var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(UserType);
|
var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(UserType);
|
||||||
var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(UserType);
|
var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(UserType);
|
||||||
|
var authenticatorProviderType = typeof(AuthenticatorTokenProvider<>).MakeGenericType(UserType);
|
||||||
return AddTokenProvider(TokenOptions.DefaultProvider, dataProtectionProviderType)
|
return AddTokenProvider(TokenOptions.DefaultProvider, dataProtectionProviderType)
|
||||||
.AddTokenProvider(TokenOptions.DefaultEmailProvider, emailTokenProviderType)
|
.AddTokenProvider(TokenOptions.DefaultEmailProvider, emailTokenProviderType)
|
||||||
.AddTokenProvider(TokenOptions.DefaultPhoneProvider, phoneNumberProviderType);
|
.AddTokenProvider(TokenOptions.DefaultPhoneProvider, phoneNumberProviderType)
|
||||||
|
.AddTokenProvider(TokenOptions.DefaultAuthenticatorProvider, authenticatorProviderType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,19 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an <see cref="IdentityError"/> indicating a recovery code was not redeemed.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="IdentityError"/> indicating a recovery code was not redeemed.</returns>
|
||||||
|
public virtual IdentityError RecoveryCodeRedemptionFailed()
|
||||||
|
{
|
||||||
|
return new IdentityError
|
||||||
|
{
|
||||||
|
Code = nameof(RecoveryCodeRedemptionFailed),
|
||||||
|
Description = Resources.RecoveryCodeRedemptionFailed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an <see cref="IdentityError"/> indicating an external login is already associated with an account.
|
/// Returns an <see cref="IdentityError"/> indicating an external login is already associated with an account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,22 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return GetString("StoreNotIRoleClaimStore");
|
return GetString("StoreNotIRoleClaimStore");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IUserAuthenticationTokenStore<User>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string StoreNotIUserAuthenticationTokenStore
|
||||||
|
{
|
||||||
|
get { return GetString("StoreNotIUserAuthenticationTokenStore"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IUserAuthenticationTokenStore<User>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatStoreNotIUserAuthenticationTokenStore()
|
||||||
|
{
|
||||||
|
return GetString("StoreNotIUserAuthenticationTokenStore");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Store does not implement IUserClaimStore<TUser>.
|
/// Store does not implement IUserClaimStore<TUser>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -570,6 +586,22 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return GetString("StoreNotIUserSecurityStampStore");
|
return GetString("StoreNotIUserSecurityStampStore");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IUserAuthenticatorKeyStore<User>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string StoreNotIUserAuthenticatorKeyStore
|
||||||
|
{
|
||||||
|
get { return GetString("StoreNotIUserAuthenticatorKeyStore"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IUserAuthenticatorKeyStore<User>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatStoreNotIUserAuthenticatorKeyStore()
|
||||||
|
{
|
||||||
|
return GetString("StoreNotIUserAuthenticatorKeyStore");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Store does not implement IUserTwoFactorStore<TUser>.
|
/// Store does not implement IUserTwoFactorStore<TUser>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -586,6 +618,22 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return GetString("StoreNotIUserTwoFactorStore");
|
return GetString("StoreNotIUserTwoFactorStore");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recovery code redemption failed.
|
||||||
|
/// </summary>
|
||||||
|
internal static string RecoveryCodeRedemptionFailed
|
||||||
|
{
|
||||||
|
get { return GetString("RecoveryCodeRedemptionFailed"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recovery code redemption failed.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatRecoveryCodeRedemptionFailed()
|
||||||
|
{
|
||||||
|
return GetString("RecoveryCodeRedemptionFailed");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User already has a password set.
|
/// User already has a password set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -682,6 +730,22 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("UserNotInRole"), p0);
|
return string.Format(CultureInfo.CurrentCulture, GetString("UserNotInRole"), p0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IUserTwoFactorRecoveryCodeStore<User>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string StoreNotIUserTwoFactorRecoveryCodeStore
|
||||||
|
{
|
||||||
|
get { return GetString("StoreNotIUserTwoFactorRecoveryCodeStore"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IUserTwoFactorRecoveryCodeStore<User>.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatStoreNotIUserTwoFactorRecoveryCodeStore()
|
||||||
|
{
|
||||||
|
return GetString("StoreNotIUserTwoFactorRecoveryCodeStore");
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,10 @@
|
||||||
<value>Store does not implement IRoleClaimStore<TRole>.</value>
|
<value>Store does not implement IRoleClaimStore<TRole>.</value>
|
||||||
<comment>Error when the store does not implement this interface</comment>
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StoreNotIUserAuthenticationTokenStore" xml:space="preserve">
|
||||||
|
<value>Store does not implement IUserAuthenticationTokenStore<User>.</value>
|
||||||
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
|
</data>
|
||||||
<data name="StoreNotIUserClaimStore" xml:space="preserve">
|
<data name="StoreNotIUserClaimStore" xml:space="preserve">
|
||||||
<value>Store does not implement IUserClaimStore<TUser>.</value>
|
<value>Store does not implement IUserClaimStore<TUser>.</value>
|
||||||
<comment>Error when the store does not implement this interface</comment>
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
|
|
@ -257,10 +261,18 @@
|
||||||
<value>Store does not implement IUserSecurityStampStore<TUser>.</value>
|
<value>Store does not implement IUserSecurityStampStore<TUser>.</value>
|
||||||
<comment>Error when the store does not implement this interface</comment>
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StoreNotIUserAuthenticatorKeyStore" xml:space="preserve">
|
||||||
|
<value>Store does not implement IUserAuthenticatorKeyStore<User>.</value>
|
||||||
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
|
</data>
|
||||||
<data name="StoreNotIUserTwoFactorStore" xml:space="preserve">
|
<data name="StoreNotIUserTwoFactorStore" xml:space="preserve">
|
||||||
<value>Store does not implement IUserTwoFactorStore<TUser>.</value>
|
<value>Store does not implement IUserTwoFactorStore<TUser>.</value>
|
||||||
<comment>Error when the store does not implement this interface</comment>
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="RecoveryCodeRedemptionFailed" xml:space="preserve">
|
||||||
|
<value>Recovery code redemption failed.</value>
|
||||||
|
<comment>Error when a recovery code is not redeemed.</comment>
|
||||||
|
</data>
|
||||||
<data name="UserAlreadyHasPassword" xml:space="preserve">
|
<data name="UserAlreadyHasPassword" xml:space="preserve">
|
||||||
<value>User already has a password set.</value>
|
<value>User already has a password set.</value>
|
||||||
<comment>Error when AddPasswordAsync called when a user already has a password</comment>
|
<comment>Error when AddPasswordAsync called when a user already has a password</comment>
|
||||||
|
|
@ -285,4 +297,8 @@
|
||||||
<value>User is not in role '{0}'.</value>
|
<value>User is not in role '{0}'.</value>
|
||||||
<comment>Error when a user is not in the role</comment>
|
<comment>Error when a user is not in the role</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StoreNotIUserTwoFactorRecoveryCodeStore" xml:space="preserve">
|
||||||
|
<value>Store does not implement IUserTwoFactorRecoveryCodeStore<User>.</value>
|
||||||
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1,21 +1,31 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Identity
|
namespace Microsoft.AspNetCore.Identity
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
internal static class Rfc6238AuthenticationService
|
internal static class Rfc6238AuthenticationService
|
||||||
{
|
{
|
||||||
private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
|
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
|
||||||
private static readonly Encoding _encoding = new UTF8Encoding(false, true);
|
private static readonly Encoding _encoding = new UTF8Encoding(false, true);
|
||||||
|
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
|
||||||
|
|
||||||
private static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
|
// Generates a new 80-bit security token
|
||||||
|
public static byte[] GenerateRandomKey()
|
||||||
|
{
|
||||||
|
byte[] bytes = new byte[20];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
|
||||||
{
|
{
|
||||||
// # of 0's = length of pin
|
// # of 0's = length of pin
|
||||||
const int Mod = 1000000;
|
const int Mod = 1000000;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
|
@ -362,6 +364,92 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme);
|
return Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signs in the user without two factor authentication using a two factor recovery code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="recoveryCode">The two factor recovery code.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
|
||||||
|
{
|
||||||
|
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
|
||||||
|
if (twoFactorInfo == null || twoFactorInfo.UserId == null)
|
||||||
|
{
|
||||||
|
return SignInResult.Failed;
|
||||||
|
}
|
||||||
|
var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return SignInResult.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
|
||||||
|
return SignInResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't protect against brute force attacks since codes are expected to be random.
|
||||||
|
return SignInResult.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient)
|
||||||
|
{
|
||||||
|
// When token is verified correctly, clear the access failed count used for lockout
|
||||||
|
await ResetLockout(user);
|
||||||
|
|
||||||
|
// Cleanup external cookie
|
||||||
|
if (twoFactorInfo.LoginProvider != null)
|
||||||
|
{
|
||||||
|
await Context.Authentication.SignOutAsync(Options.Cookies.ExternalCookieAuthenticationScheme);
|
||||||
|
}
|
||||||
|
// Cleanup two factor user id cookie
|
||||||
|
await Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme);
|
||||||
|
if (rememberClient)
|
||||||
|
{
|
||||||
|
await RememberTwoFactorClientAsync(user);
|
||||||
|
}
|
||||||
|
await SignInAsync(user, isPersistent, twoFactorInfo.LoginProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the sign in code from an authenticator app and creates and signs in the user, as an asynchronous operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">The two factor authentication code to validate.</param>
|
||||||
|
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
|
||||||
|
/// <param name="rememberClient">Flag indicating whether the current browser should be remember, suppressing all further
|
||||||
|
/// two factor authentication prompts.</param>
|
||||||
|
/// <returns>The task object representing the asynchronous operation containing the <see name="SignInResult"/>
|
||||||
|
/// for the sign-in attempt.</returns>
|
||||||
|
public virtual async Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string code, bool isPersistent, bool rememberClient)
|
||||||
|
{
|
||||||
|
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
|
||||||
|
if (twoFactorInfo == null || twoFactorInfo.UserId == null)
|
||||||
|
{
|
||||||
|
return SignInResult.Failed;
|
||||||
|
}
|
||||||
|
var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return SignInResult.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = await PreSignInCheck(user);
|
||||||
|
if (error != null)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await UserManager.VerifyTwoFactorTokenAsync(user, Options.Tokens.AuthenticatorTokenProvider, code))
|
||||||
|
{
|
||||||
|
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
|
||||||
|
return SignInResult.Success;
|
||||||
|
}
|
||||||
|
// If the token is incorrect, record the failure which also may cause the user to be locked out
|
||||||
|
await UserManager.AccessFailedAsync(user);
|
||||||
|
return SignInResult.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates the two faction sign in code and creates and signs in the user, as an asynchronous operation.
|
/// Validates the two faction sign in code and creates and signs in the user, as an asynchronous operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -393,21 +481,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
}
|
}
|
||||||
if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code))
|
if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code))
|
||||||
{
|
{
|
||||||
// When token is verified correctly, clear the access failed count used for lockout
|
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
|
||||||
await ResetLockout(user);
|
|
||||||
// Cleanup external cookie
|
|
||||||
if (twoFactorInfo.LoginProvider != null)
|
|
||||||
{
|
|
||||||
await Context.Authentication.SignOutAsync(Options.Cookies.ExternalCookieAuthenticationScheme);
|
|
||||||
}
|
|
||||||
// Cleanup two factor user id cookie
|
|
||||||
await Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme);
|
|
||||||
if (rememberClient)
|
|
||||||
{
|
|
||||||
await RememberTwoFactorClientAsync(user);
|
|
||||||
}
|
|
||||||
await UserManager.ResetAccessFailedCountAsync(user);
|
|
||||||
await SignInAsync(user, isPersistent, twoFactorInfo.LoginProvider);
|
|
||||||
return SignInResult.Success;
|
return SignInResult.Success;
|
||||||
}
|
}
|
||||||
// If the token is incorrect, record the failure which also may cause the user to be locked out
|
// If the token is incorrect, record the failure which also may cause the user to be locked out
|
||||||
|
|
@ -599,7 +673,6 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<SignInResult> SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false)
|
private async Task<SignInResult> SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false)
|
||||||
{
|
{
|
||||||
if (!bypassTwoFactor &&
|
if (!bypassTwoFactor &&
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly string DefaultPhoneProvider = "Phone";
|
public static readonly string DefaultPhoneProvider = "Phone";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default token provider name used by the <see cref="AuthenticatorTokenProvider{TUser}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string DefaultAuthenticatorProvider = "Authenticator";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Will be used to construct UserTokenProviders with the key used as the providerName.
|
/// Will be used to construct UserTokenProviders with the key used as the providerName.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -54,5 +59,13 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// The <see cref="ChangeEmailTokenProvider"/> used to generate tokens used in email change confirmation emails.
|
/// The <see cref="ChangeEmailTokenProvider"/> used to generate tokens used in email change confirmation emails.
|
||||||
/// </value>
|
/// </value>
|
||||||
public string ChangeEmailTokenProvider { get; set; } = DefaultProvider;
|
public string ChangeEmailTokenProvider { get; set; } = DefaultProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="AuthenticatorTokenProvider"/> used to validate two factor sign ins with an authenticator.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The <see cref="AuthenticatorTokenProvider"/> used to validate two factor sign ins with an authenticator.
|
||||||
|
/// </value>
|
||||||
|
public string AuthenticatorTokenProvider { get; set; } = DefaultAuthenticatorProvider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// Gets a flag indicating whether the backing user store supports authentication tokens.
|
/// Gets a flag indicating whether the backing user store supports authentication tokens.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true if the backing user store supports authentication tokens, otherwise false.
|
/// true if the backing user store supports authentication tokens, otherwise false.
|
||||||
/// </value>
|
/// </value>
|
||||||
public virtual bool SupportsUserAuthenticationTokens
|
public virtual bool SupportsUserAuthenticationTokens
|
||||||
{
|
{
|
||||||
|
|
@ -169,6 +169,36 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a flag indicating whether the backing user store supports a user authenticator.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true if the backing user store supports a user authenticatior, otherwise false.
|
||||||
|
/// </value>
|
||||||
|
public virtual bool SupportsUserAuthenticatorKey
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return Store is IUserAuthenticatorKeyStore<TUser>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a flag indicating whether the backing user store supports recovery codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true if the backing user store supports a user authenticatior, otherwise false.
|
||||||
|
/// </value>
|
||||||
|
public virtual bool SupportsUserTwoFactorRecoveryCodes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return Store is IUserTwoFactorRecoveryCodeStore<TUser>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a flag indicating whether the backing user store supports two factor authentication.
|
/// Gets a flag indicating whether the backing user store supports two factor authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -2013,11 +2043,11 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
|
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
|
||||||
/// <param name="tokenName">The name of the token.</param>
|
/// <param name="tokenName">The name of the token.</param>
|
||||||
/// <returns></returns>
|
/// <returns>The authentication token for a user</returns>
|
||||||
public virtual Task<string> GetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
|
public virtual Task<string> GetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
var store = GetTokenStore();
|
var store = GetAuthenticationTokenStore();
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
|
@ -2041,11 +2071,11 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
|
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
|
||||||
/// <param name="tokenName">The name of the token.</param>
|
/// <param name="tokenName">The name of the token.</param>
|
||||||
/// <param name="tokenValue">The value of the token.</param>
|
/// <param name="tokenValue">The value of the token.</param>
|
||||||
/// <returns></returns>
|
/// <returns>Whether the user was successfully updated.</returns>
|
||||||
public virtual async Task<IdentityResult> SetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName, string tokenValue)
|
public virtual async Task<IdentityResult> SetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName, string tokenValue)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
var store = GetTokenStore();
|
var store = GetAuthenticationTokenStore();
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
|
@ -2074,7 +2104,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
public virtual async Task<IdentityResult> RemoveAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
|
public virtual async Task<IdentityResult> RemoveAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
var store = GetTokenStore();
|
var store = GetAuthenticationTokenStore();
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
|
@ -2092,6 +2122,110 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return await UpdateUserAsync(user);
|
return await UpdateUserAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the authenticator key for the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>The authenticator key</returns>
|
||||||
|
public virtual Task<string> GetAuthenticatorKeyAsync(TUser user)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var store = GetAuthenticatorKeyStore();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
return store.GetAuthenticatorKeyAsync(user, CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the authenticator key for the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>Whether the user was successfully updated.</returns>
|
||||||
|
public virtual async Task<IdentityResult> ResetAuthenticatorKeyAsync(TUser user)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var store = GetAuthenticatorKeyStore();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
await store.SetAuthenticatorKeyAsync(user, GenerateNewAuthenticatorKey(), CancellationToken);
|
||||||
|
return await UpdateAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a new base32 encoded 160-bit security secret (size of SHA1 hash).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The new security secret.</returns>
|
||||||
|
public virtual string GenerateNewAuthenticatorKey()
|
||||||
|
{
|
||||||
|
return Base32.ToBase32(Rfc6238AuthenticationService.GenerateRandomKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates recovery codes for the user, this invalidates any previous recovery codes for the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to generate recovery codes for.</param>
|
||||||
|
/// <param name="number">The number of codes to generate.</param>
|
||||||
|
/// <returns>The new recovery codes for the user.</returns>
|
||||||
|
public virtual async Task<IEnumerable<string>> GenerateNewTwoFactorRecoveryCodesAsync(TUser user, int number)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var store = GetRecoveryCodeStore();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCodes = new List<string>(number);
|
||||||
|
for (var i = 0; i < number; i++)
|
||||||
|
{
|
||||||
|
newCodes.Add(CreateTwoFactorRecoveryCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.ReplaceCodesAsync(user, newCodes, CancellationToken);
|
||||||
|
var update = await UpdateAsync(user);
|
||||||
|
if (update.Succeeded)
|
||||||
|
{
|
||||||
|
return newCodes;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new recovery code.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual string CreateTwoFactorRecoveryCode()
|
||||||
|
{
|
||||||
|
return Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid
|
||||||
|
/// once, and will be invalid after use.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user who owns the recovery code.</param>
|
||||||
|
/// <param name="code">The recovery code to use.</param>
|
||||||
|
/// <returns>True if the recovery code was found for the user.</returns>
|
||||||
|
public virtual async Task<IdentityResult> RedeemTwoFactorRecoveryCodeAsync(TUser user, string code)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var store = GetRecoveryCodeStore();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await store.RedeemCodeAsync(user, code, CancellationToken);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return await UpdateAsync(user);
|
||||||
|
}
|
||||||
|
return IdentityResult.Failed(ErrorDescriber.RecoveryCodeRedemptionFailed());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases the unmanaged resources used by the role manager and optionally releases the managed resources.
|
/// Releases the unmanaged resources used by the role manager and optionally releases the managed resources.
|
||||||
|
|
@ -2292,12 +2426,32 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return await Store.UpdateAsync(user, CancellationToken);
|
return await Store.UpdateAsync(user, CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IUserAuthenticationTokenStore<TUser> GetTokenStore()
|
private IUserAuthenticatorKeyStore<TUser> GetAuthenticatorKeyStore()
|
||||||
|
{
|
||||||
|
var cast = Store as IUserAuthenticatorKeyStore<TUser>;
|
||||||
|
if (cast == null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(Resources.StoreNotIUserAuthenticatorKeyStore);
|
||||||
|
}
|
||||||
|
return cast;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUserTwoFactorRecoveryCodeStore<TUser> GetRecoveryCodeStore()
|
||||||
|
{
|
||||||
|
var cast = Store as IUserTwoFactorRecoveryCodeStore<TUser>;
|
||||||
|
if (cast == null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(Resources.StoreNotIUserTwoFactorRecoveryCodeStore);
|
||||||
|
}
|
||||||
|
return cast;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUserAuthenticationTokenStore<TUser> GetAuthenticationTokenStore()
|
||||||
{
|
{
|
||||||
var cast = Store as IUserAuthenticationTokenStore<TUser>;
|
var cast = Store as IUserAuthenticationTokenStore<TUser>;
|
||||||
if (cast == null)
|
if (cast == null)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Resources.StoreNotIUserAuthenticationTokenStore");
|
throw new NotSupportedException(Resources.StoreNotIUserAuthenticationTokenStore);
|
||||||
}
|
}
|
||||||
return cast;
|
return cast;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
||||||
IUserTwoFactorStore<TUser>,
|
IUserTwoFactorStore<TUser>,
|
||||||
IQueryableRoleStore<TRole>,
|
IQueryableRoleStore<TRole>,
|
||||||
IRoleClaimStore<TRole>,
|
IRoleClaimStore<TRole>,
|
||||||
IUserAuthenticationTokenStore<TUser>
|
IUserAuthenticationTokenStore<TUser>,
|
||||||
|
IUserAuthenticatorKeyStore<TUser>,
|
||||||
|
IUserTwoFactorRecoveryCodeStore<TUser>
|
||||||
where TRole : TestRole
|
where TRole : TestRole
|
||||||
where TUser : TestUser
|
where TUser : TestUser
|
||||||
{
|
{
|
||||||
|
|
@ -548,6 +550,39 @@ namespace Microsoft.AspNetCore.Identity.InMemory
|
||||||
return Task.FromResult(tokenEntity?.TokenValue);
|
return Task.FromResult(tokenEntity?.TokenValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string AuthenticatorStoreLoginProvider = "[AspNetAuthenticatorStore]";
|
||||||
|
private const string AuthenticatorKeyTokenName = "AuthenticatorKey";
|
||||||
|
private const string RecoveryCodeTokenName = "RecoveryCodes";
|
||||||
|
|
||||||
|
public Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return SetTokenAsync(user, AuthenticatorStoreLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return GetTokenAsync(user, AuthenticatorStoreLoginProvider, AuthenticatorKeyTokenName, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReplaceCodesAsync(TUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var mergedCodes = string.Join(";", recoveryCodes);
|
||||||
|
return SetTokenAsync(user, AuthenticatorStoreLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var mergedCodes = await GetTokenAsync(user, AuthenticatorStoreLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? "";
|
||||||
|
var splitCodes = mergedCodes.Split(';');
|
||||||
|
if (splitCodes.Contains(code))
|
||||||
|
{
|
||||||
|
var updatedCodes = new List<string>(splitCodes.Where(s => s != code));
|
||||||
|
await ReplaceCodesAsync(user, updatedCodes, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public IQueryable<TRole> Roles
|
public IQueryable<TRole> Roles
|
||||||
{
|
{
|
||||||
get { return _roles.Values.AsQueryable(); }
|
get { return _roles.Values.AsQueryable(); }
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
var provider = services.BuildServiceProvider();
|
||||||
var tokenProviders = provider.GetRequiredService<IOptions<IdentityOptions>>().Value.Tokens.ProviderMap.Values;
|
var tokenProviders = provider.GetRequiredService<IOptions<IdentityOptions>>().Value.Tokens.ProviderMap.Values;
|
||||||
Assert.Equal(3, tokenProviders.Count());
|
Assert.Equal(4, tokenProviders.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,111 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
auth.Verify();
|
auth.Verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class GoodTokenProvider : AuthenticatorTokenProvider<TestUser>
|
||||||
|
{
|
||||||
|
public override Task<bool> ValidateAsync(string purpose, string token, UserManager<TestUser> manager, TestUser user)
|
||||||
|
{
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, true, true)]
|
||||||
|
[InlineData("Authenticator", false, true)]
|
||||||
|
[InlineData("Gooblygook", true, false)]
|
||||||
|
[InlineData("--", false, false)]
|
||||||
|
public async Task CanTwoFactorAuthenticatorSignIn(string providerName, bool isPersistent, bool rememberClient)
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
var user = new TestUser { UserName = "Foo" };
|
||||||
|
const string code = "3123";
|
||||||
|
var manager = SetupUserManager(user);
|
||||||
|
manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
|
||||||
|
manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(true).Verifiable();
|
||||||
|
manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
|
||||||
|
|
||||||
|
var context = new Mock<HttpContext>();
|
||||||
|
var auth = new Mock<AuthenticationManager>();
|
||||||
|
var twoFactorInfo = new SignInManager<TestUser>.TwoFactorAuthenticationInfo { UserId = user.Id };
|
||||||
|
var helper = SetupSignInManager(manager.Object, context.Object);
|
||||||
|
if (providerName != null)
|
||||||
|
{
|
||||||
|
helper.Options.Tokens.AuthenticatorTokenProvider = providerName;
|
||||||
|
}
|
||||||
|
var id = helper.StoreTwoFactorInfo(user.Id, null);
|
||||||
|
SetupSignIn(auth, user.Id, isPersistent);
|
||||||
|
auth.Setup(a => a.AuthenticateAsync(helper.Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme)).ReturnsAsync(id).Verifiable();
|
||||||
|
context.Setup(c => c.Authentication).Returns(auth.Object).Verifiable();
|
||||||
|
if (rememberClient)
|
||||||
|
{
|
||||||
|
auth.Setup(a => a.SignInAsync(
|
||||||
|
helper.Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme,
|
||||||
|
It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.Name) == user.Id
|
||||||
|
&& i.Identities.First().AuthenticationType == helper.Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme),
|
||||||
|
It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await helper.TwoFactorAuthenticatorSignInAsync(code, isPersistent, rememberClient);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
manager.Verify();
|
||||||
|
context.Verify();
|
||||||
|
auth.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, true)]
|
||||||
|
[InlineData(true, false)]
|
||||||
|
[InlineData(false, true)]
|
||||||
|
[InlineData(false, false)]
|
||||||
|
public async Task CanTwoFactorRecoveryCodeSignIn(bool supportsLockout, bool externalLogin)
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
var user = new TestUser { UserName = "Foo" };
|
||||||
|
const string bypassCode = "someCode";
|
||||||
|
var manager = SetupUserManager(user);
|
||||||
|
manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable();
|
||||||
|
manager.Setup(m => m.RedeemTwoFactorRecoveryCodeAsync(user, bypassCode)).ReturnsAsync(IdentityResult.Success).Verifiable();
|
||||||
|
if (supportsLockout)
|
||||||
|
{
|
||||||
|
manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
|
||||||
|
}
|
||||||
|
var context = new Mock<HttpContext>();
|
||||||
|
var auth = new Mock<AuthenticationManager>();
|
||||||
|
var twoFactorInfo = new SignInManager<TestUser>.TwoFactorAuthenticationInfo { UserId = user.Id };
|
||||||
|
var loginProvider = "loginprovider";
|
||||||
|
var helper = SetupSignInManager(manager.Object, context.Object);
|
||||||
|
var id = helper.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null);
|
||||||
|
if (externalLogin)
|
||||||
|
{
|
||||||
|
auth.Setup(a => a.SignInAsync(
|
||||||
|
helper.Options.Cookies.ApplicationCookieAuthenticationScheme,
|
||||||
|
It.Is<ClaimsPrincipal>(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider
|
||||||
|
&& i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id),
|
||||||
|
It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
|
||||||
|
auth.Setup(a => a.SignOutAsync(helper.Options.Cookies.ExternalCookieAuthenticationScheme)).Returns(Task.FromResult(0)).Verifiable();
|
||||||
|
auth.Setup(a => a.SignOutAsync(helper.Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme)).Returns(Task.FromResult(0)).Verifiable();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetupSignIn(auth, user.Id);
|
||||||
|
}
|
||||||
|
auth.Setup(a => a.AuthenticateAsync(helper.Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme)).ReturnsAsync(id).Verifiable();
|
||||||
|
context.Setup(c => c.Authentication).Returns(auth.Object).Verifiable();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await helper.TwoFactorRecoveryCodeSignInAsync(bypassCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.Succeeded);
|
||||||
|
manager.Verify();
|
||||||
|
context.Verify();
|
||||||
|
auth.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true, true)]
|
[InlineData(true, true)]
|
||||||
[InlineData(true, false)]
|
[InlineData(true, false)]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -691,6 +693,146 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.ThrowsAsync<NotImplementedException>(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose"));
|
Assert.ThrowsAsync<NotImplementedException>(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TOTPTest()
|
||||||
|
{
|
||||||
|
//var verify = new TotpAuthenticatorVerification();
|
||||||
|
//var secret = "abcdefghij";
|
||||||
|
//var secret = Base32.FromBase32(authKey);
|
||||||
|
|
||||||
|
// Assert.Equal(bytes, secret);
|
||||||
|
|
||||||
|
//var code = verify.VerifyCode(secret, -1);
|
||||||
|
//Assert.Equal(code, 287004);
|
||||||
|
|
||||||
|
|
||||||
|
//var bytes = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'!', (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF };
|
||||||
|
//var base32 = Base32.ToBase32(bytes);
|
||||||
|
// var code = Rfc6238AuthenticationService.GenerateCode(bytes);
|
||||||
|
// Assert.Equal(Rfc6238AuthenticationService.GenerateCode(bytes), Rfc6238AuthenticationService.CalculateOneTimePassword(new HMACSHA1(bytes)));
|
||||||
|
//Assert.True(Rfc6238AuthenticationService.ValidateCode(bytes, code));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] ToBytes(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("input");
|
||||||
|
}
|
||||||
|
|
||||||
|
input = input.TrimEnd('='); //remove padding characters
|
||||||
|
int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
|
||||||
|
byte[] returnArray = new byte[byteCount];
|
||||||
|
|
||||||
|
byte curByte = 0, bitsRemaining = 8;
|
||||||
|
int mask = 0, arrayIndex = 0;
|
||||||
|
|
||||||
|
foreach (char c in input)
|
||||||
|
{
|
||||||
|
int cValue = CharToValue(c);
|
||||||
|
|
||||||
|
if (bitsRemaining > 5)
|
||||||
|
{
|
||||||
|
mask = cValue << (bitsRemaining - 5);
|
||||||
|
curByte = (byte)(curByte | mask);
|
||||||
|
bitsRemaining -= 5;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mask = cValue >> (5 - bitsRemaining);
|
||||||
|
curByte = (byte)(curByte | mask);
|
||||||
|
returnArray[arrayIndex++] = curByte;
|
||||||
|
curByte = (byte)(cValue << (3 + bitsRemaining));
|
||||||
|
bitsRemaining += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if we didn't end with a full byte
|
||||||
|
if (arrayIndex != byteCount)
|
||||||
|
{
|
||||||
|
returnArray[arrayIndex] = curByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToString(byte[] input)
|
||||||
|
{
|
||||||
|
if (input == null || input.Length == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("input");
|
||||||
|
}
|
||||||
|
|
||||||
|
int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
|
||||||
|
char[] returnArray = new char[charCount];
|
||||||
|
|
||||||
|
byte nextChar = 0, bitsRemaining = 5;
|
||||||
|
int arrayIndex = 0;
|
||||||
|
|
||||||
|
foreach (byte b in input)
|
||||||
|
{
|
||||||
|
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
|
||||||
|
returnArray[arrayIndex++] = ValueToChar(nextChar);
|
||||||
|
|
||||||
|
if (bitsRemaining < 4)
|
||||||
|
{
|
||||||
|
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
|
||||||
|
returnArray[arrayIndex++] = ValueToChar(nextChar);
|
||||||
|
bitsRemaining += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitsRemaining -= 3;
|
||||||
|
nextChar = (byte)((b << bitsRemaining) & 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
//if we didn't end with a full char
|
||||||
|
if (arrayIndex != charCount)
|
||||||
|
{
|
||||||
|
returnArray[arrayIndex++] = ValueToChar(nextChar);
|
||||||
|
while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(returnArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CharToValue(char c)
|
||||||
|
{
|
||||||
|
var value = (int)c;
|
||||||
|
|
||||||
|
//65-90 == uppercase letters
|
||||||
|
if (value < 91 && value > 64)
|
||||||
|
{
|
||||||
|
return value - 65;
|
||||||
|
}
|
||||||
|
//50-55 == numbers 2-7
|
||||||
|
if (value < 56 && value > 49)
|
||||||
|
{
|
||||||
|
return value - 24;
|
||||||
|
}
|
||||||
|
//97-122 == lowercase letters
|
||||||
|
if (value < 123 && value > 96)
|
||||||
|
{
|
||||||
|
return value - 97;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException("Character is not a Base32 character.", "c");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char ValueToChar(byte b)
|
||||||
|
{
|
||||||
|
if (b < 26)
|
||||||
|
{
|
||||||
|
return (char)(b + 65);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b < 32)
|
||||||
|
{
|
||||||
|
return (char)(b + 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException("Byte is not a value Base32 value.", "b");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UserManagerWillUseTokenProviderInstanceOverDefaults()
|
public void UserManagerWillUseTokenProviderInstanceOverDefaults()
|
||||||
{
|
{
|
||||||
|
|
@ -741,6 +883,43 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
await Assert.ThrowsAsync<NotSupportedException>(async () => await manager.IsInRoleAsync(null, "bogus"));
|
await Assert.ThrowsAsync<NotSupportedException>(async () => await manager.IsInRoleAsync(null, "bogus"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthTokenMethodsFailWhenStoreNotImplemented()
|
||||||
|
{
|
||||||
|
var error = Resources.StoreNotIUserAuthenticationTokenStore;
|
||||||
|
var manager = MockHelpers.TestUserManager(new NoopUserStore());
|
||||||
|
Assert.False(manager.SupportsUserAuthenticationTokens);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.GetAuthenticationTokenAsync(null, null, null), error);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.SetAuthenticationTokenAsync(null, null, null, null), error);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.RemoveAuthenticationTokenAsync(null, null, null), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticatorMethodsFailWhenStoreNotImplemented()
|
||||||
|
{
|
||||||
|
var error = Resources.StoreNotIUserAuthenticatorKeyStore;
|
||||||
|
var manager = MockHelpers.TestUserManager(new NoopUserStore());
|
||||||
|
Assert.False(manager.SupportsUserAuthenticatorKey);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.GetAuthenticatorKeyAsync(null), error);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.ResetAuthenticatorKeyAsync(null), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RecoveryMethodsFailWhenStoreNotImplemented()
|
||||||
|
{
|
||||||
|
var error = Resources.StoreNotIUserTwoFactorRecoveryCodeStore;
|
||||||
|
var manager = MockHelpers.TestUserManager(new NoopUserStore());
|
||||||
|
Assert.False(manager.SupportsUserTwoFactorRecoveryCodes);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.RedeemTwoFactorRecoveryCodeAsync(null, null), error);
|
||||||
|
await VerifyException<NotSupportedException>(async () => await manager.GenerateNewTwoFactorRecoveryCodesAsync(null, 10), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task VerifyException<TException>(Func<Task> code, string expectedMessage) where TException : Exception
|
||||||
|
{
|
||||||
|
var error = await Assert.ThrowsAsync<TException>(code);
|
||||||
|
Assert.Equal(expectedMessage, error.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisposeAfterDisposeDoesNotThrow()
|
public void DisposeAfterDisposeDoesNotThrow()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue