I use asp net core Identity. This is what I am trying to do and I have no Idea how to do this so need some expert help on this. When a new user registers in my application , a password reset link is sent to the person's email, Now when the link token in the link has expired and the user clicks on the link I need to show a message as token expired. How do I find out If the token has expired or not when the user clicks on the link which is in the email. Can any one suggest ways to achieve this scenario?
The ASP.NET Core has a built-in Claims system to store information about the user .
To do that ,
add a helper method to store the expiration datetime in Register.cshtml.cs:
private async Task AddTokenExpirationInfo(IdentityUser user, int span=1*24*60)
{
var expiresAt = DateTime.Now.Add(TimeSpan.FromMinutes(span));
var tokenExpiredAtClaim = new Claim("ActivtationTokenExpiredAt", expiresAt.ToUniversalTime().Ticks.ToString());
await _userManager.AddClaimAsync(user, tokenExpiredAtClaim);
}
add a helper method to check whether the token has expired in the ConfirmEmail.cshtml.cs :
private async Task<bool> TokenExpiredValidate(IdentityUser user) {
var claims = (await _userManager.GetClaimsAsync(user))
.Where(c => c.Type == "ActivtationTokenExpiredAt");
var expiredAt = claims.FirstOrDefault()?.Value;
bool expired = true; // default value
if (expiredAt != null)
{
var expires = Convert.ToInt64(expiredAt);
var now = DateTime.Now.Ticks;
expired= now <= expires? false : true;
}
else {
expired = false;
}
// clear claims
await _userManager.RemoveClaimsAsync(user, claims);
return expired;
}
invoke AddTokenExpirationInfo when there's a registeration request :
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
///////////////// invoke here ////////////////////
AddTokenExpirationInfo(user);
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
// If we got this far, something failed, redisplay form
return Page();
}
invoke TokenExpiredValidate in your ConfirmEmail.cshtml.cs:
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
if (await TokenExpiredValidate(user))
throw new InvalidOperationException($"Token has alread expired '{userId}':");
return Page();
}
When a user registers , there' will be a record in the AspNetUserClaims table :
When a user confirmed successfully , the record will be removed . Just as a reminder , a more robust method is to use a background service to clear the expired record .
Related
I want to make an email confirmation callback link from asp.net core web API repository class,
This is easy when done in the controller of the API, but I am having trouble doing this from the repository class.
I keep getting this error:
Could not find an IRouter associated with the ActionContext. If your application is using endpoint routing then you can get a IUrlHelperFactory with dependency injection and use it to create a UrlHelper, or use Microsoft.AspNetCore.Routing.LinkGenerator.
this is what I have so far:
this is my repository code for registration:
public async Task<(bool IsSuccess, string ErrorMessage)> Register(RegisterDTO model)
{
if (model != null)
{
var user = _mapper.Map<ApplicationUser>(model);
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, "Admin");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
string _scheme = _urlHelper.ActionContext.HttpContext.Request.Scheme;
//var callbackurl = _urlHelper.Link("ConfirmEmail", new { email =
user.Email, code = code });
var callbackurl =
_urlHelper.Action("ConfirmEmail",nameof(AccountController), new { email = user.Email, code = code }, _scheme);
var mailresult = _mailSender.SendEmail(model.Email, "Please confirm your
account by clicking here: link");
return (true, "Account created successfully");
}
return (false, "Eror occured while creating your account");
}
return (false, "Please provide the user data");
}
Account Controller
[HttpGet("{token}/{email}")]
public async Task<IActionResult> ConfirmEmail(string token, string email)
{
var result = await _repository.ConfirmEmail(token, email);
if (result.IsSuccess)
return Ok(result.ErrorMessage);
else
return BadRequest(result.ErrorMessage);
}
I managed to get it to work.
see the code below:
enter code here
public async Task<(bool IsSuccess, string ErrorMessage)>
Register(RegisterDTO model)
{
if (model != null)
{
var user = _mapper.Map<ApplicationUser>(model);
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, "Admin");
var _urlHelper = _urlHelperFactory.GetUrlHelper(
_actionContextAccessor.ActionContext);
var code = await _userManager.
GenerateEmailConfirmationTokenAsync(user);
string _scheme = _urlHelper.ActionContext.
HttpContext.Request.Scheme;
var callbackurl = _urlHelper.Action(action:
nameof(AccountController.ConfirmEmail),
controller: "Account", values:
new { email = user.Email, code = code },
protocol: _scheme);
var mailresult = _mailSender.SendEmail(model.Email, "Please
confirm your account by clicking here: <a href=\"" +
callbackurl + "\">link</a>");
if (mailresult.IsSuccess)
{
return (true, "Account created successfully, Please visit
your email to confirm your email");
}
else
{
return (true, $"Account created successfully,
{mailresult.ErrorMessage}");
}
}
return (false, result.Errors.ToString());
}
return (false, "Please provide the user data");
}
This API is intended for a mobile application. The goal is to let the user confirm the email upon registration. When the user registers, a confirmation link is generated and sent over the email. I've done it the same way in a MVC project, it worked fine, but in a Web API project looks like it ain't gonna cut.
Now when the user clicks that link, the respective action method should be hit and do the job.
The only problem is, the ConfirmEmail action method is just not getting triggered when clicking the confirmation link although it looked fine.
Here are the main configurations which might help
MVC service configuration
services.AddMvc(options =>
{
options.EnableEndpointRouting = true;
options.Filters.Add<ValidationFilter>();
})
.AddFluentValidation(mvcConfiguration => mvcConfiguration.RegisterValidatorsFromAssemblyContaining<Startup>())
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
Identity Service
public async Task<AuthenticationResult> RegisterAsync(string email, string password)
{
var existingUser = await _userManager.FindByEmailAsync(email);
if(existingUser != null)
{
return new AuthenticationResult { Errors = new[] { "User with this email address exists" } };
}
// generate user
var newUser = new AppUser
{
Email = email,
UserName = email
};
// register user in system
var result = await _userManager.CreateAsync(newUser, password);
if (!result.Succeeded)
{
return new AuthenticationResult
{
Errors = result.Errors.Select(x => x.Description)
};
}
// when registering user, assign him user role, also need to be added in the JWT!!!
await _userManager.AddToRoleAsync(newUser, "User");
// force user to confirm email, generate token
var token = await _userManager.GenerateEmailConfirmationTokenAsync(newUser);
// generate url
var confirmationLink = _urlHelper.Action("ConfirmEmail", "IdentityController",
new { userId = newUser.Id, token = token }, _httpRequest.HttpContext.Request.Scheme);
// send it per email
var mailresult =
await _emailService.SendEmail(newUser.Email, "BingoApp Email Confirmation",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(confirmationLink)}'>clicking here</a>.");
if (mailresult)
return new AuthenticationResult { Success = true };
else
return new AuthenticationResult { Success = false, Errors = new List<string> { "Invalid Email Address"} };
}
Controller
[HttpPost(ApiRoutes.Identity.Register)]
public async Task<IActionResult> Register([FromBody] UserRegistrationRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(new AuthFailedResponse
{
Errors = ModelState.Values.SelectMany(x => x.Errors.Select(xx => xx.ErrorMessage))
});
}
// register the incoming user data with identity service
var authResponse = await _identityService.RegisterAsync(request.Email, request.Password);
if (!authResponse.Success)
{
return BadRequest(new AuthFailedResponse
{
Errors = authResponse.Errors
});
}
// confirm registration
return Ok();
}
[HttpGet]
public async Task<IActionResult> ConfirmEmail(string userId, string token)
{
if (userId == null || token == null)
{
return null;
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return null;
}
var result = await _userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
await _emailService.SendEmail(user.Email, "BingoApp - Successfully Registered", "Congratulations,\n You have successfully activated your account!\n " +
"Welcome to the dark side.");
}
return null;
}
Your _urlHelper.Action(..) looks a bit suspicious to me.
I'm not sure you should pass the full controller name, that is, including the actual word controller.
Try _urlHelper.Action("ConfirmEmail", "Identity", instead.
As a tip: I try to avoid magic strings like these by using nameof(IdentityController) because it will return the controller name without the controller postfix.
I'm writing api for the application
The task is to redirect to the profile page on the client after confirming the email. While email is not confirmed, I can not give access token to api.
At the moment a crutch lives on the client: username and password
temporarily stored locally, so that after the customer can make a hidden sign in.(what is not safe)
How can this problem be solved?
I have one not very nice idea: after registration give a access token and after on for each request check - emailConfirmed(bool)
But - perform an additional request each time ...
Small code example from back:
ApplicationOAuthProvider:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
var a = await userManager.FindByIdAsync(context.ClientId);
if (user == null)
{
context.SetError("invalid_grant", "Username or Password is incorrect.");
return;
}
if (!user.EmailConfirmed)
{
context.SetError("invalid_grant", "Email not confirmed.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
Send code on email:
[AllowAnonymous]
[HttpPost]
[Route("Users/{userId}/emailConfirmation")]
public async Task<IHttpActionResult> SendConfirmationCode(string userId)
{
if (userId == null)
{
return BadRequest("UserId were not transferred");
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return Content(HttpStatusCode.NotFound, "User not found");
}
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var url = $"{clientBaseUrl}confirm_email?userId={user.Id}&code={WebUtility.UrlEncode(code)}";
await MailHelper.SendConfirmationLinkOnEmail(url, user.Email);
return Content(HttpStatusCode.OK, new { Message = "Confirmation code was sent on email" });
}
Email confirmation by code:
[AllowAnonymous]
[HttpPost]
[Route("Users/{userId}/emailConfirmationCode")]
public async Task<IHttpActionResult> ConfirmEmail(string userId, [FromBody]EmailConfirmationDTO input)
{
if (userId == null || input == null)
{
return BadRequest("UserId or code were not transferred");
}
try
{
var isConfirmed = await UserManager.IsEmailConfirmedAsync(userId);
if (isConfirmed)
{
return Content(HttpStatusCode.Forbidden, new { Message = "This user already confirmed email" });
}
var result = await UserManager.ConfirmEmailAsync(userId, input.Code);
if (result.Succeeded)
{
var user = Db.Users.FirstOrDefaultAsync(u => u.Id == userId);
return Content(HttpStatusCode.OK, new { responseObj = user });
}
}
catch (Exception e)
{
// ignored
}
return Content(HttpStatusCode.InternalServerError, new { Message = "Error confirmation." });
}
ConfigureAuth:
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(5),
#if (!DEBUG)
AllowInsecureHttp = false
#else
AllowInsecureHttp = true
#endif
};
How to change user password by admin in Asp core 2.x ?
or Change password with sms code
My sample code:
if (!ModelState.IsValid)
return View(model);
var user = await _userManager.FindByNameAsync(model.UserName);
if (user == null)
return RedirectToAction("Index");
if (model.smsCode == user.SmsCode)
{
user.PasswordHash = model.NewPassword;
IdentityResult result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
}
}
error: save unhash pass in db
We should not update the user.PasswordHash with a plain text , we should use Hash instead .
var user = await _userManager.FindByNameAsync(model.UserName);
if(user == null){ /**/ }
if (model.smsCode != user.SmsCode){ /**/}
// compute the new hash string
var newPassword = _userManager.PasswordHasher.HashPassword(user,newpass);
user.PasswordHash = newPassword;
var res = await _userManager.UpdateAsync(user);
if (res.Succeeded) {/**/}
else { /**/}
I'm working on an API in which I have implemented Login and ResetPassword functionality. Login works fine and resetPassword also works fine. But when I reset my password and try to login using the new password, login is failing. Upon reset i can see the hash and password fields getting updated but login fails. I'm using the following code for reset.
if (user.VerificationCode == model.VerificationCode)
{
//var newPasswordHash = UserManager.PasswordHasher.HashPassword(model.NewPassword);
//var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
//user.PasswordHash = newPasswordHash;
user.Password = model.NewPassword;
//IdentityResult result = await UserManager.ResetPasswordAsync(user.Id, token, newPasswordHash);
//IdentityResult result = await UserManager.ChangePasswordAsync(user.Id, user.Password, model.NewPassword);
var result = await UserManager.UpdateAsync(user);
if (!result.Succeeded)
{
response.Message = AppConstants.Error;
response.IsSuccess = false;
}
else
{
response.Message = AppConstants.OperationSuccessful;
response.IsSuccess = true;
}
}
And for login the already given code using SignInManager.PasswordSignInAsync.
Any ideas what is being done wrong here?
If you want to use UpdateAsync method, you should hash password first:
This method worked for me:
public async Task<IHttpActionResult> changePassword(UsercredentialsModel usermodel)
{
ApplicationUser user = await AppUserManager.FindByIdAsync(usermodel.Id);
if (user == null)
{
return NotFound();
}
user.PasswordHash = AppUserManager.PasswordHasher.HashPassword(usermodel.Password);
var result = await AppUserManager.UpdateAsync(user);
if (!result.Succeeded)
{
//throw exception......
}
return Ok();
}
You can aslo use RemovePasswordAsync and AddPasswordAsync together to change password:
await UserManager.RemovePasswordAsync(userId.Id);
var result = UserManager.AddPasswordAsync(NewPassword);