I configured Web app in ASP.NET Core 3.1 to use ASP.NET Core Identity with local accounts. Everything works correctly. I want to force user to insert password (or use external provider) again when he go to specific page e.g. account settings. I cannot find example with this case. Does somebody has experience with this? Maybe there is some article which I missed.
So far the only idea is to log out user and open login page but it's not logic because he should be able to open other pages without restrictions.
I've not tried this but I just stumbled across this blog post which sounds like it would work for you, I've kinda shortened it down to an almost TL;DR post
Setup the required password verification page
The RequirePasswordVerificationModel class implements the Razor page which requires that a user has verified a password for the identity user within the last ten minutes.
public class RequirePasswordVerificationModel : PasswordVerificationBase
{
public RequirePasswordVerificationModel(UserManager<ApplicationUser> userManager) : base(userManager)
{
}
public async Task<IActionResult> OnGetAsync()
{
var passwordVerificationOk = await ValidatePasswordVerification();
if (!passwordVerificationOk)
{
return RedirectToPage("/PasswordVerification",
new { ReturnUrl = "/DoUserChecks/RequirePasswordVerification" });
}
return Page();
}
}
The PasswordVerificationBase Razor page implements the PageModel. The ValidatePasswordVerification method checks if the user is already authenticated
public class PasswordVerificationBase : PageModel
{
public static string PasswordCheckedClaimType = "passwordChecked";
private readonly UserManager<ApplicationUser> _userManager;
public PasswordVerificationBase(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<bool> ValidatePasswordVerification()
{
if (User.Identity.IsAuthenticated)
{
if (User.HasClaim(c => c.Type == PasswordCheckedClaimType))
{
var user = await _userManager.FindByEmailAsync(User.Identity.Name);
var lastLogin = DateTime.FromFileTimeUtc(
Convert.ToInt64(user.LastLogin));
var lastPasswordVerificationClaim
= User.FindFirst(PasswordCheckedClaimType);
var lastPasswordVerification = DateTime.FromFileTimeUtc(
Convert.ToInt64(lastPasswordVerificationClaim.Value));
if (lastLogin > lastPasswordVerification)
{
return false;
}
else if (DateTime.UtcNow.AddMinutes(-10.0) > lastPasswordVerification)
{
return false;
}
return true;
}
}
return false;
}
}
If the user needs to re-enter credentials, the PasswordVerificationModel Razor page is used for this. This class was built using the identity scaffolded login Razor page from ASP.NET Core Identity. The old password verifications claims are removed using the UserManager service. A new password verification claim is created, if the user successfully re-entered the password and the sign in is refreshed with the new ClaimIdentity instance.
public class PasswordVerificationModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<PasswordVerificationModel> _logger;
public PasswordVerificationModel(SignInManager<ApplicationUser> signInManager,
ILogger<PasswordVerificationModel> logger,
UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
[BindProperty]
public CheckModel Input { get; set; }
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public string ReturnUrl { get; set; }
[TempData]
public string ErrorMessage { get; set; }
public class CheckModel
{
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
public async Task<IActionResult> OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if (!hasPassword)
{
return NotFound($"User has no password'{_userManager.GetUserId(User)}'.");
}
returnUrl ??= Url.Content("~/");
ReturnUrl = returnUrl;
return Page();
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(user.Email, Input.Password, false, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User password re-entered");
await RemovePasswordCheck(user);
var claim = new Claim(PasswordVerificationBase.PasswordCheckedClaimType,
DateTime.UtcNow.ToFileTimeUtc().ToString());
await _userManager.AddClaimAsync(user, claim);
await _signInManager.RefreshSignInAsync(user);
return LocalRedirect(returnUrl);
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
private async Task RemovePasswordCheck(ApplicationUser user)
{
if (User.HasClaim(c => c.Type == PasswordVerificationBase.PasswordCheckedClaimType))
{
var claims = User.FindAll(PasswordVerificationBase.PasswordCheckedClaimType);
foreach (Claim c in claims)
{
await _userManager.RemoveClaimAsync(user, c);
}
}
}
}
The PasswordVerificationModel Razor page html template displays the user input form with the password field.
@page
@model PasswordVerificationModel
@{
ViewData["Title"] = "Password Verification";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<section>
<form id="account" method="post">
<h4>Verify account using your password</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password"
class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
Re-enter password
</button>
</div>
</form>
</section>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
The Login Razor page needs to be updated to add a login file time value for DateTime.UtcNow when the login successfully occurred. This value is used in the base Razor page to verify the password check. The LastLogin property was added for this.
var result = await _signInManager.PasswordSignInAsync(
Input.Email, Input.Password,
Input.RememberMe,
lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
var user = await _userManager.FindByEmailAsync(Input.Email);
if (user == null)
{
return NotFound("help....");
}
user.LastLogin = DateTime.UtcNow.ToFileTimeUtc().ToString();
var lastLoginResult = await _userManager.UpdateAsync(user);
return LocalRedirect(returnUrl);
}
The LastLogin property was added to the ApplicationUser which implements the IdentityUser. This value is persisted to the Entity Framework Core database.
public class ApplicationUser : IdentityUser
{
public string LastLogin { get; set; }
}
When the application is started, the user can login and will need to verify a password to access the Razor page implemented to require this feature.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments