how to revoke/invalidate/cancel old email confirmation token (identity) - asp.net

i allow newly created users who know their password and isn't confirmed yet to change their registration email (as long as it's not registered in my database)
the problem is that if they changed the email, i generate new email confirmation token, but the old token could still validate them(the one i issue on registration), which pretty much could mean that people could use their registration mail at first, change it to some other mail they don't have access to, and validate from the old one, which is a big security hole for me to just leave
is there any way to remove/revoke the old token? (technically i could create a new user and delete the old one, the old token wouldn't work on new user, yet i think there should be a better solution for this)

I added the following properties to my ApplicationUser class
public class ApplicationUser : IdentityUser {
public string EmailConfirmationToken { get; set; }
public string ResetPasswordToken { get; set; }
}
This holds on to the confirmation token to be validated against when confirming email token.
I then added the following to my ApplicationUserManager which is a UserManager<ApplicationUser> derived class.
public override async System.Threading.Tasks.Task<string> GenerateEmailConfirmationTokenAsync(string userId) {
/* NOTE:
* The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
* (like when the user's password changes), the tokens will always be the same, and remain valid.
* So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
*/
//await base.UpdateSecurityStampAsync(userId);
var token = await base.GenerateEmailConfirmationTokenAsync(userId);
if (!string.IsNullOrEmpty(token)) {
var user = await FindByIdAsync(userId);
user.EmailConfirmationToken = token;
user.EmailConfirmed = false;
await UpdateAsync(user);
}
return token;
}
public override async System.Threading.Tasks.Task<string> GeneratePasswordResetTokenAsync(string userId) {
var token = await base.GeneratePasswordResetTokenAsync(userId);
if (!string.IsNullOrEmpty(token)) {
var x = await FindByIdAsync(userId);
x.ResetPasswordToken = token;
await UpdateAsync(x);
}
return token;
}
public override async System.Threading.Tasks.Task<IdentityResult> ConfirmEmailAsync(string userId, string token) {
var result = await base.ConfirmEmailAsync(userId, token);
if (result.Succeeded) {
var x = await FindByIdAsync(userId);
x.EmailConfirmationToken = null;
await UpdateAsync(x);
}
return result;
}
public override async System.Threading.Tasks.Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword) {
var result = await base.ResetPasswordAsync(userId, token, newPassword);
if (result.Succeeded) {
var x = await FindByIdAsync(userId);
x.ResetPasswordToken = null;
await UpdateAsync(x);
}
return result;
}
The following Extensions were added to be able to find the user based on their stored token.
public static class ApplicationUserManagerExtension {
public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken) {
string result = null;
ApplicationUser user = manager.Users.SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);
if (user != null) {
result = user.Id;
}
return Task.FromResult(result);
}
public static Task<string> FindIdByResetPasswordTokenAsync(this UserManager<ApplicationUser> manager, string token) {
string result = null;
ApplicationUser user = manager.Users.SingleOrDefault(u => u.ResetPasswordToken != null && u.ResetPasswordToken == token);
if (user != null) {
result = user.Id;
}
return Task.FromResult(result);
}
}

Related

Validation Email with .Net core IDENTITY always returns Invalid token

I have a problem with my validation email method. I only user default Identity configuration.
I send an email like this :
[HttpPost("register")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> RegisterAsync(UserRegisterViewModel model)
{
//// creattion of user
var createdUser = await _userRepository.GetUserByEmailAsync(user.Email);
string titleMail = "My title";
string textMail = "My Body";
string url = await GenerateEmailConfirmationUrl(createdUser);
await _emailHelper.SendMailAsync(new Helpers.Models.SendMailModel()
{
To = new List<string>() { model.Email },
Title = titleMail,
Body = $"{textMail} <a href='{url}'>ICI</a>",
});
/// etc ...
}
private async Task<string> GenerateEmailConfirmationUrl(UserDto user)
{
var token = await _authRepository.GetEmailConfirmationTokenAsync(user);
string url = $"{_configuration.GetSection("AppSettings:ApplicationUrl").Value}/auth/email-validation?email={user.Email}&token={Base64UrlEncoder.Encode(token)}";
return url;
}
RepositoryFile
public async Task<string> GetEmailConfirmationTokenAsync(User user)
{
return await _userManager.GenerateEmailConfirmationTokenAsync(user);
}
And this how I do the validation :
[HttpGet]
[Route("email-validation")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> EmailValidation(string email, string token)
{
var user = await _authRepository.GetUserByEmail(email);
if (user == null) return Unauthorized();
var decryptedToken = Base64UrlEncoder.Decode(token);
var result = await _authRepository.EmailValidationAsync(user, decryptedToken);
if (result.Succeeded)
return Ok();
return BadRequest(result.Errors);
}
When I check what I send and what I get I see that the token are exactly the same but
await _userManager.ConfirmEmailAsync(_mapper.Map(user), token);
always return InvalidToken error and I really don't understand why.

Store additional info when creating new user in IdentityStore in web api

I am building/learning token based authentication with OWIN and I would like to figure out how to insert additional information when creating a new user. The UserManager accepts IdentityUser, but the CreateAsync method only accepts a user name and passowrd. I would like to add at least the email address. I see that there is a SetEmailAsync method, but that requires a second call. I feel like there should be a single call that allows me to insert other columns, but I am not finding any documentation of how to do this, nor closely related questions in StackOverflow.
Here is the save routine:
public class AuthRepository : IDisposable
{
private readonly AuthContext _context;
private readonly UserManager<IdentityUser> _userManager;
public AuthRepository()
{
_context = new AuthContext();
_userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(_context));
}
public async Task<IdentityUser> FindUserAsync(string userName, string password)
{
IdentityUser user = await _userManager.FindAsync(userName, password);
return user;
}
public async Task<IdentityResult> RegisterUserAsync(UserModel userModel)
{
var user = new IdentityUser
{
UserName = userModel.UserName
};
//save all of this in one call?
var result = await _userManager.CreateAsync(user, userModel.Password);
var result1 = await _userManager.SetEmailAsync(userModel.UserName, userModel.EmailAddress);
return result;
}
public async Task<IdentityUser> FindIdentityUserAsync(string userName, string password)
{
var user = await _userManager.FindAsync(userName, password);
return user;
}
public void Dispose()
{
_context.Dispose();
_userManager.Dispose();
}
}
you can create your own User class by inheriting IdentityUser class.
public class User : IdentityUser
{
public string Email { get; set; }
}
var user = new User
{
UserName = userModel.UserName,
Email = userModel.EmailAddress
};
var result = await _userManager.CreateAsync(user, userModel.Password);
Make sure you are using User Class instead of IdentityUser.

Identity Framework test if confirm email token is expired

Is it possible to test whether a confirm email token is expired using Identity Framework's UserManager? No matter what the error is, from the following:
var result = await UserManager.ConfirmEmailAsync(userId, code);
I get a generic "Invalid Token" error.
I found a way to parse the token for the date issued, which you can then check to see if is within the allowed timespan (default of 24hours if not specified).
Identity.cs
ApplicationUserManager
public IDataProtector Protector { get; set; }
public TimeSpan TokenLifespan { get; set; }
ApplicationUserManager Create()
// Explicitly set token expiration to 24 hours.
manager.TokenLifespan = TimeSpan.FromHours(24);
var dataProtectionProvider = options.DataProtectionProvider;
manager.Protector = dataProtectionProvider.Create("ASP.NET Identity");
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = manager.TokenLifespan
};
}
AccountController.cs
public async Task<ActionResult> ConfirmEmail(string Code, string UserId)
{
// Try/catch, validation, etc.
var tokenExpired = false;
var unprotectedData = UserManager.Protector.Unprotect(Convert.FromBase64String(Code));
var ms = new MemoryStream(unprotectedData);
using (BinaryReader reader = new BinaryReader(ms))
{
var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
var expirationTime = creationTime + UserManager.TokenLifespan;
if (expirationTime < DateTimeOffset.UtcNow)
{
tokenExpired = true;
}
}
// Do something if token is expired, else continue with confirmation
}
I found this blog post and Nkosi's answer to be extremely helpful, and if you want to go through the Identity source code, Microsoft has it here (The previous versions of Identity for MVC5 and lower here). Also, I apologize if its in poor form to answer a question that you, yourself put a bounty on, but I couldn't help but continue looking for a better solution.
I get around this by keeping/storing a copy of the generated token
public class ApplicationUser : IdentityUser {
public string EmailConfirmationToken { get; set; }
public string ResetPasswordToken { get; set; }
}
and associating it with the user in derived UserManager<ApplicationUser>.
public override async System.Threading.Tasks.Task<string> GenerateEmailConfirmationTokenAsync(string userId) {
/* NOTE:
* The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
* (like when the user's password changes), the tokens will always be the same, and remain valid.
* So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
*/
//await base.UpdateSecurityStampAsync(userId);
var token = await base.GenerateEmailConfirmationTokenAsync(userId);
if (!string.IsNullOrEmpty(token)) {
var user = await FindByIdAsync(userId);
user.EmailConfirmationToken = token; //<<< Last issued token
//Note: If a token is generated then the current email is no longer confirmed.
user.EmailConfirmed = false;
await UpdateAsync(user);
}
return token;
}
When the token is provided for confirmation, a search for the user via the token is done.
public static class ApplicationUserManagerExtension {
public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken) {
string result = null;
ApplicationUser user = manager.Users.SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);
if (user != null) {
result = user.Id;
}
return Task.FromResult(result);
}
}
If the token matches a known user that indicates that it was a validly issued token.
Will then attempt to confirm token with User manager.
If confirmation fails then token has expired and an appropriate action is taken.
Else if the token confirmed, it is removed from associated user and thus invalidating the reuse of that token.
public override async System.Threading.Tasks.Task<IdentityResult> ConfirmEmailAsync(string userId, string token) {
var user = await FindByIdAsync(userId);
if (user == null) {
return IdentityResult.Failed("User Id Not Found");
}
var result = await base.ConfirmEmailAsync(userId, token);
if (result.Succeeded) {
user.EmailConfirmationToken = null;
return await UpdateAsync(user);
} else if (user.EmailConfirmationToken == token) {
//Previously Issued Token expired
result = IdentityResult.Failed("Expired Token");
}
return result;
}
A similar approach was implemented for password reset as well.
Here comes an .NET Core 2.1 adaption of the solution provided by #Nkosi :
ApplicationUser class
public class ApplicationUser : IdentityUser
{
public string EmailConfirmationToken { get; set; }
public string ResetPasswordToken { get; set; }
}
Derived UserManager class
public class CustomUserManager : UserManager<ApplicationUser>
{
public CustomUserManager(IUserStore<ApplicationUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<ApplicationUser> passwordHasher,
IEnumerable<IUserValidator<ApplicationUser>> userValidators,
IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<ApplicationUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
}
public override async Task<string> GenerateEmailConfirmationTokenAsync(ApplicationUser user)
{
/* NOTE:
* The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
* (like when the user's password changes), the tokens will always be the same, and remain valid.
* So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
*/
//await base.UpdateSecurityStampAsync(userId);
var token = await base.GenerateEmailConfirmationTokenAsync(user);
if (!string.IsNullOrEmpty(token))
{
user.EmailConfirmationToken = token; //<<< Last issued token
//Note: If a token is generated then the current email is no longer confirmed.
user.EmailConfirmed = false;
await UpdateAsync(user);
}
return token;
}
public override async Task<IdentityResult> ConfirmEmailAsync(ApplicationUser user, string token)
{
if (user == null)
{
return IdentityResult.Failed(new IdentityError {Description = "User not found."});
}
var result = await base.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
user.EmailConfirmationToken = null;
return await UpdateAsync(user);
}
else if (user.EmailConfirmationToken == token)
{
//Previously Issued Token expired
result = IdentityResult.Failed(new IdentityError { Description = "Expired token." });
}
return result;
}
}
UserManager Extension
public static class ApplicationUserManagerExtension
{
public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken)
{
string result = null;
ApplicationUser user = manager.Users
.SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);
if (user != null)
{
result = user.Id;
}
return Task.FromResult(result);
}
}
Update:
The CustomUserManager has to be added to services in Startup.cs in ConfigureServices Method.
services.AddTransient<CustomUserManager>();
Without this, DependencyInjection fails.
You can use my controller.It's working mate.
public IActionResult ForgotPassword()
{
return View();
}
[HttpPost]
public async Task<IActionResult> ForgotPassword(string Email)
{
if (string.IsNullOrEmpty(Email))
{
return View();
}
var user = await _userManager.FindByEmailAsync(Email);
if (user == null)
{
return View();
}
var code =await _userManager.GeneratePasswordResetTokenAsync(user);
var callback = Url.Action("ResetPassword", "Account", new
{
token=code,
},Request.Scheme);
// send email
await _emailSender.SendEmailAsync(Email, "Confirm Password Reset", $"<a href='{callback}'>If you want to reset your password click please !</a>");
return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
public IActionResult ForgotPasswordConfirmation() => View();
public IActionResult ResetPassword(string token)
{
if (token == null)
{
return View();
}
var model = new ResetPasswordModel()
{
Token = token,
};
return View(model);
}
[HttpPost]
public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
return RedirectToAction("Index", "Home");
}
var result = await _userManager.ResetPasswordAsync(user, model.Token, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
return View(model);
}
public ActionResult ResetPasswordConfirmation() => View();

ApplicationUser object for Facebook or other Social Media Logins in Ap.net Core

How can we access the properties of the ApplicationUser from the Account Controller in. While creating a an ApplicationUser object with User, object reference set to null error occurs.The example code is
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
return View(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
_logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
//I want to access the ApplicationUser Properites here
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
You should be able to access the ApplicationUser like this...
//I want to access the ApplicationUser Properites here
var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);

ASP.Net Identity and Google Authentication Issue

I have setup the project in Google and it gave me the appid and secret
I moved the id and secret to StartUp.Auth
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.CreatePerOwinContext<IdentityTestingDbContext>(IdentityTestingDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseGoogleAuthentication(
clientId: "*********************.apps.googleusercontent.com ",
clientSecret: "**************");
}
}
Here are the actions for external login, i am following Identity Sample Application (install-package Microsoft.AspNet.Identity.Samples -Pre).
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
var challenge = new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
return challenge;
}
// Used for XSRF protection when adding external logins
private const string XsrfKey = "XsrfId";
internal class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null)
{
}
public ChallengeResult(string provider, string redirectUri, string userId)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
}
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
var user = await UserManager.FindAsync(loginInfo.Login);
if (user == null)
{
user = new ApplicationUser
{
Email = loginInfo.Email,
UserName = loginInfo.DefaultUserName,
FirstName = string.Empty,
LastName = string.Empty
};
var result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return View("Error", result.Errors);
}
result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
if (!result.Succeeded)
{
return View("Error", result.Errors);
}
}
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
identity.AddClaims(loginInfo.ExternalIdentity.Claims);
AuthenticationManager.SignIn(new AuthenticationProperties
{
IsPersistent = false
}, identity);
return Redirect(returnUrl ?? "/");
}
I get redirected to google but here i am getting an error. Looks like i am missing something but can't figure it out. I have been searching for almost 3 hours and couldn't find any thing to help with this issue.
Do you see any thing that i may be doing wrong?
Why redirect url in the image below is http://localhost:58286/signin-google
Following helped
http://www.asp.net/mvc/overview/security/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on
Fix 1:
The authorized redirect url needs to be http://localhost:58286/signin-google, for the google setup screen shot in the above question thread. This isn't the callback method inside the accounts controller.
Fix 2:
I needed to enable Google+ API as well which i didn't during the setup

Resources