I don't believe there is any functionality to allow the user to reset their password at the login screen if they forget it. This existed in the UserAppService of the old framework where an email could be sent out to the user and a PasswordResetCode stored in the database.
It would be really nice if this could be added to this framework, and work with the EmailConfirmed property so new users can register and change their password by clicking on a link in the email sent.
Maybe the open source identity module will not include these features for the time being.
If you need you can implement it yourself, because Microsoft's Identity project template already provides almost all the code.
Thanks for the feature request. We will consider this for the next versions. However, I suggest you to implement yourself if it is needed for your application.
@hikalkan @maliming assign me to this one I will try to implement this weak.
Sure, it is yours :)
I took the advice of maliming and implemented a version of this myself the other day. It won't be perfect but it might help you @mkmita? There is some stuff missing like adding a new PasswordResetToken property to AbpUser/IdentityUser but it works fine from testing.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Emailing;
using Volo.Abp.Identity;
using Serilog;
using MyCompanyName.MyProjectName.Localization;
namespace MyCompanyName.MyProjectName.Account
{
/// <summary>
/// Sits along-side the Volo.Abp.Account.IAccountAppService - extending it to include password reset functionality. This should be moved/implemented in Volo.Abp.Account.AccountAppService
/// </summary>
public class AccountAppService : ApplicationService, IAccountAppService
{
// todo: move this to Domain.Shared project?
public const string PasswordResetToken = "PasswordResetToken";
private readonly IdentityUserManager _userManager;
private readonly IEmailSender _emailSender;
public AccountAppService(
IdentityUserManager userManager,
IEmailSender emailSender)
{
LocalizationResource = typeof(MyProjectNameResource);
_userManager = userManager;
_emailSender = emailSender;
}
// POST /api/account/reset-password-request
public async Task ResetPasswordRequest(ResetPasswordRequestInput input)
{
var user = await _userManager.FindByEmailAsync(input.EmailAddress);
// For security, don't indicate that the user can't be found
if (user == null) return;
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
var body = L[
"ResetPasswordRequest:EmailBody",
// not required, but is used to personalize the email and reduces spam rating
user.Name,
input.ReturnUrl.RemovePostFix("/"),
System.Web.HttpUtility.UrlEncode(resetToken),
user.TenantId,
user.Id,
// not required, but is used by the angular application to allow the user to
// be automatically logged in after resetting the password without calling the api again to search for the user by their Id.
System.Web.HttpUtility.UrlEncode(user.Email)
];
try
{
await _emailSender.SendAsync(user.Email, L["ResetPasswordRequest:EmailSubject"], body);
}
catch(Exception e)
{
Log.Error(e, "ResetPasswordRequest failed! Please ensure the default SMTP settings are valid.");
throw new UserFriendlyException("Unable to process your request, please try again later.");
}
// Do this last to avoid saving the token if errors occur above i.e. the message fails to send
// todo: Instead of using user.ExtraProperties, Implement AbpUser.PasswordResetToken property instead!
if (user.ExtraProperties.ContainsKey(PasswordResetToken))
user.ExtraProperties[PasswordResetToken] = resetToken;
else
user.ExtraProperties.Add(PasswordResetToken, resetToken);
await _userManager.UpdateAsync(user);
}
// POST /api/account/reset-password
public virtual async Task ResetPassword(ResetPasswordInput input)
{
var user = await _userManager.FindByIdAsync(input.UserId.ToString());
if (user == null || !user.ExtraProperties.TryGetValue(PasswordResetToken, out var resetToken) || ((string)resetToken) != input.Token)
{
throw new UserFriendlyException("Invalid token supplied", null, "The token may have expired, please try resetting your password again.");
}
// Throws an exception if the token is invalid
(await _userManager.ResetPasswordAsync(user, (string)resetToken, input.NewPassword)).CheckErrors();
// todo: I would like to automatically confirm the users email after restting their password but..
// can't use 'user.EmailConfirmed = true;' and need email token to confirm the email when using the _userManager.
// The only way to do it currently is to use 'GenerateChangeEmailTokenAsync'
//await _userManager.ConfirmEmailAsync(user, await _userManager.GenerateChangeEmailTokenAsync(user, user.Email));
user.ExtraProperties.Remove(PasswordResetToken);
await _userManager.UpdateAsync(user);
}
}
}
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace MyCompanyName.MyProjectName.Account
{
public interface IAccountAppService : IApplicationService
{
Task ResetPasswordRequest(ResetPasswordRequestInput input);
Task ResetPassword(ResetPasswordInput input);
}
}
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using MyProjectName.Account;
namespace MyCompanyName.MyProjectName.Controllers
{
[RemoteService]
[Controller]
[Area("account")]
[Route("api/account")]
public class AccountController : MyProjectNameController, IAccountAppService
{
private readonly IAccountAppService _accountAppService;
public AccountController(IAccountAppService accountAppService)
{
_accountAppService = accountAppService;
}
[HttpPost]
[Route("reset-password-request")]
public virtual Task ResetPasswordRequest(ResetPasswordRequestInput input)
{
// Try to guess the return url from the Referer
// todo: this feature is not required but may be useful?
if (input.ReturnUrl.IsNullOrEmpty())
{
// Create a URL without the Query or Fragment parts
if (Request.Headers.ContainsKey("Referer") && Uri.TryCreate(Request.Headers["Referer"].ToString(), UriKind.Absolute, out var parsedReturnUrl))
{
input.ReturnUrl = parsedReturnUrl.GetLeftPart(UriPartial.Path);
}
else
{
throw new UserFriendlyException("Invalid returnUrl specified");
}
}
else if (!Uri.IsWellFormedUriString(input.ReturnUrl, UriKind.Absolute))
{
throw new UserFriendlyException("Invalid returnUrl specified");
}
return _accountAppService.ResetPasswordRequest(input);
}
[HttpPost]
[Route("reset-password")]
public virtual Task ResetPassword([FromBody]ResetPasswordInput input)
{
return _accountAppService.ResetPassword(input);
}
}
}
public class ResetPasswordRequestInput
{
public string EmailAddress { get; set; }
public string ReturnUrl { get; set; }
}
public class ResetPasswordInput
{
public Guid UserId { get; set; }
public string Token { get; set; }
public string NewPassword { get; set; }
}
{
"ResetPasswordRequest:EmailBody": "Hi {0}, to reset your password please <a href=\"{1}?t={2}&tid={3}&uid={4}\" target=\"_blank\">click here</a>.",
"ResetPasswordRequest:EmailSubject": "Reset your password"
}
{
"Settings": {
"Abp.Mailing.DefaultFromAddress": "team@localhost",
"Abp.Mailing.DefaultFromDisplayName": "MyProjectName Team",
"Abp.Mailing.Smtp.Host": "localhost",
"Abp.Mailing.Smtp.Port": "25",
"Abp.Mailing.Smtp.UserName": "noreply@localhost",
"Abp.Mailing.Smtp.Password": "1q2w3e*",
"Abp.Mailing.Smtp.EnableSsl": "false"
}
}
@olicooper yes this will help thank you, sir.
Keep in mind that the value Abp.Mailing.Smtp.Password must be encrypted.
Also check if the property ReturnUrl on the ResetPasswordRequestInput model is on you're own website and not a malicious website.
I think only relative return url's from the client should be accepted and on the server the correct full address must be created which will be used in the e-mail.
Any update on this? Also, since alot of the identity related functionality is available in the default asp templates, why can't we just have it all also in abp? I have tried modifying the identity module and the account module to include all default functionality available after identity scaffolding i.e. 2fa, forgot password, email conform etc. Will need some help to verify the implementation, still new to abp, i don't know if anyone is willing to assist, so we can do a pull request.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using Volo.Abp.Emailing;
using Volo.Abp.Identity.Settings;
using Volo.Abp.Settings;
using Volo.Abp.Users;
namespace Volo.Abp.Identity
{
[Authorize]
public class ProfileAppService : IdentityAppServiceBase, IProfileAppService
{
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
private readonly IdentityUserManager _userManager;
private readonly IEmailSender _emailSender;
public ProfileAppService(IdentityUserManager userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
}
public virtual async Task<ProfileDto> GetAsync()
{
return ObjectMapper.Map<IdentityUser, ProfileDto>(
await _userManager.GetByIdAsync(CurrentUser.GetId())
);
}
public virtual async Task<ProfileDto> UpdateAsync(UpdateProfileDto input)
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (await SettingProvider.IsTrueAsync(IdentitySettingNames.User.IsUserNameUpdateEnabled))
{
(await _userManager.SetUserNameAsync(user, input.UserName)).CheckErrors();
}
if (await SettingProvider.IsTrueAsync(IdentitySettingNames.User.IsEmailUpdateEnabled))
{
(await _userManager.SetEmailAsync(user, input.Email)).CheckErrors();
}
(await _userManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors();
user.Name = input.Name;
user.Surname = input.Surname;
(await _userManager.UpdateAsync(user)).CheckErrors();
await CurrentUnitOfWork.SaveChangesAsync();
return ObjectMapper.Map<IdentityUser, ProfileDto>(user);
}
public virtual async Task<IdentityResult> ChangeEmailConfirmationAsync(string userId, string newEmail, string code)
{
if (userId == null || newEmail == null || code == null)
{
throw new UserFriendlyException("Missing parameters", "400");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new UserFriendlyException($"Unable to load user with ID '{userId}'.");
}
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
return await _userManager.ChangeEmailAsync(user, newEmail, code);
}
public virtual async Task<IdentityResult> ChangeEmailAsync(string oldEmail, string newEmail)
{
if (string.IsNullOrWhiteSpace(oldEmail))
{
return IdentityResult.Failed();
}
var user = await _userManager.FindByEmailAsync(oldEmail);
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return IdentityResult.Failed();
}
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
string code = await _userManager.GenerateChangeEmailTokenAsync(user, newEmail);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
string userId = await _userManager.GetUserIdAsync(user);
await _emailSender.SendAsync(
newEmail,
"Conform Email Change",
$"Please confirm your new by <a href='{HtmlEncoder.Default.Encode($"/change-email-confirmation/{userId}/{code}")}'>clicking here</a>.");
return IdentityResult.Success;
}
public virtual async Task ChangePasswordAsync(ChangePasswordInput input)
{
var currentUser = await _userManager.GetByIdAsync(CurrentUser.GetId());
(await _userManager.ChangePasswordAsync(currentUser, input.CurrentPassword, input.NewPassword)).CheckErrors();
}
public async Task<bool> Check2faAsync()
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (user == null)
{
throw new UserFriendlyException("User not found");
}
bool check2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
if (!check2faEnabled)
{
throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{user.Id}' as it's not currently enabled.");
}
return true;
}
public async Task<IdentityResult> SetTwoFactorDisabledAsync()
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (user == null)
{
throw new UserFriendlyException("User not found");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return disable2faResult;
}
public async Task<Dictionary<string, string>> DownloadPersonalData()
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (user == null)
{
throw new UserFriendlyException("User not found");
}
// Only include personal data for download
var personalDataProps = typeof(IdentityUser).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
return personalDataProps.ToDictionary(p => p.Name, p => p.GetValue(user)?.ToString() ?? "null");
//Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
//return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");
}
public async Task<SharedKeyAndQrCodeDto> LoadSharedKeyAndQrCodeUriAsync()
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
// Load the authenticator key & QR code URI to display on the form
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
string email = await _userManager.GetEmailAsync(user);
return new SharedKeyAndQrCodeDto(FormatKey(unformattedKey), GenerateQrCodeUri(email, unformattedKey));
}
public async Task<IdentityResult> SetTwoFactorEnabledAsync(SetTwoFactorInput input)
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (user == null)
{
throw new UserFriendlyException("User not found");
}
// Strip spaces and hypens
string verificationCode = input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
bool is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
// await LoadSharedKeyAndQrCodeUriAsync(user);
return IdentityResult.Failed();
}
return await _userManager.SetTwoFactorEnabledAsync(user, true);
}
public async Task<SetTwoFactorEnableDto> GenerateNewTwoFactorRecoveryCodesAsync()
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (user == null)
{
throw new UserFriendlyException("User not found");
}
bool isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
string userId = await _userManager.GetUserIdAsync(user);
if (!isTwoFactorEnabled)
{
throw new UserFriendlyException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled.");
}
const string statusMessage = "You have generated new recovery codes.";
return await _userManager.CountRecoveryCodesAsync(user) != 0 ? new SetTwoFactorEnableDto(null, false, "Failed") : new SetTwoFactorEnableDto(await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10), true, statusMessage);
}
public async Task<IdentityResult> ResetAuthenticatorKeyAsync()
{
var user = await _userManager.GetByIdAsync(CurrentUser.GetId());
if (user == null)
{
throw new UserFriendlyException($"Unable to load user'.");
}
bool isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
string userId = await _userManager.GetUserIdAsync(user);
if (!isTwoFactorEnabled)
{
throw new UserFriendlyException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled.");
}
var result = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (result.Succeeded)
{
return await _userManager.ResetAuthenticatorKeyAsync(user);
}
return IdentityResult.Failed();
//_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
//StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
}
#region Utils
private static string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private static string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
AuthenticatorUriFormat,
System.Net.WebUtility.UrlEncode("XKode"), //TODO: Replace with Application Name, preferably from configuration
System.Net.WebUtility.UrlEncode(email),
unformattedKey);
}
#endregion
}
}
thks
I am taking this to v3.2.
Most helpful comment
@hikalkan @maliming assign me to this one I will try to implement this weak.