I am using this method for Reset Password
public ResetTokenResult DoPasswordResetTokenForChange(string userId, string token)
{
switch (UserManager.FindById(userId))
{
case null:
return ResetTokenResult.UnknownUserId;
case CatalystUser user when ! (user.PasswordInvalidatedByReset ?? false):
return ResetTokenResult.TokenIsExpired;
case CatalystUser user when ! ((user.PasswordResetTokenExpiration ?? DateTime.MinValue) > DateTime.UtcNow):
return ResetTokenResult.TokenIsExpired;
case CatalystUser user when UserManager.VerifyUserToken(user.Id, "ResetPassword", token):
user.PasswordResetTokenExpiration = DateTime.UtcNow.AddDays(-1); // 1-time use. Invalidate now.UserManager.Update(user);
return ResetTokenResult.Success;
default:
return ResetTokenResult.InvalidToken;
}
}
Controller which I am using this method
[RequireHttpsWhenConfigured]
public async Task<ActionResult> Index(PasswordChangePage currentPage,
string userId, string token, string returnUrl = "")
{
var model = new PasswordChangePageViewModel(currentPage);
var isResetPasswordRequest = !string.IsNullOrEmpty(userId) && !string.IsNullOrEmpty(token);
if (!isResetPasswordRequest)
{
if (!RequestContext.IsCurrentUserAuthorized())
return Redirect(NavigationService.GetLoginLink());
model.PasswordChangeModel = new PasswordChangeViewModel {ReturnUrl = returnUrl};
model.ReturnUrl = returnUrl;
return View("Index", model);
}
if (RequestContext.IsCurrentUserAuthorized())
{
SignInManager.AuthenticationManager.SignOut();
return Redirect(Request.Url?.AbsoluteUri ?? "~/");
}
var loginLink = NavigationService.GetLoginLink();
var result = UserAccountService.DoPasswordResetTokenForChange(userId,Base64ForUrlDecode(token));
if ((result & ResetTokenResult.Failure) != ResetTokenResult.None)
{
model.ChangeCanProceed = false;
model.ErrorMessage = GetMessageForTokenResult(result);
model.LoginLink = loginLink;
}
else
{
model.PasswordChangeModel = new PasswordChangeViewModel { CurrentPassword = "null", IsResetPassword = true, UserId = userId, ResetPasswordToken = token };
model.ReturnUrl = loginLink;
}
return View("Index", model);
}
When users want to reset their password, they receive an email with a token link and everything works fine. As I know default ASPNET Identity token burns after 1 clicking to link.
My question is what is the best way to implement logic, the token link will burn after 5 clickings to link which is sent to email.
Related
First of all, i have check other solutions here too but it doesn't seem to solve my problem.
The problem is, everything is working ok. If a user clicks on "Forget Password" and enters his username. He is sent an email with reset password link. Now if he clicks on the url, he is directed to the url but the resetpassword page isn't being loaded, instead the other condition, which is HttpNotFound() page is being loaded. Seriously need to know what is going wrong with my code.
This is my ForgotPassword action
[HttpPost]
public ActionResult ForgotPassword(string username)
{
string message = "";
using (MBNSystemEntities db = new MBNSystemEntities())
{
var userdetails = db.Users.Where(x => x.UserName == username).FirstOrDefault();
if (userdetails != null)
{
string validationKey = Guid.NewGuid().ToString().Substring(0, 8);
string validationPin = Guid.NewGuid().ToString().Substring(0, 4);
SendMail(userdetails.Email, validationKey, "ResetPassword");
UserValidationRequest uvr = new UserValidationRequest();
uvr.UserId = userdetails.UserId;
uvr.ValidationType = 1;
uvr.ValidationExpiryDate = DateTime.Now.AddDays(1);
uvr.ValidationKey = validationKey;
uvr.ValidationPin = validationPin;
uvr.ValidationStatus = 0;
db.UserValidationRequests.Add(uvr);
db.SaveChanges();
message = "Reset Password link has been sent to your email id.";
}
else
{
message = "Account Not Found";
}
}
ViewBag.Message = message;
return View();
}
And this is my ResetPassword action
public ActionResult ResetPassword(string validationKey)
{
if(string.IsNullOrWhiteSpace(validationKey))
{
return HttpNotFound();
}
using (MBNSystemEntities db = new MBNSystemEntities())
{
var user = db.UserValidationRequests.Where(x => x.ValidationKey == validationKey).FirstOrDefault();
if (user != null)
{
ResetPasswordModel model = new ResetPasswordModel();
model.validationKey = validationKey;
return View(model);
}
else
{
return HttpNotFound();
}
}
}
I checked by putting breakpoint. The validationKey is passed null in Forgetpassword(string validationkey) instead of the actual validationkey generated in ForgotPassword action.
This is the email Sample:
your email sent is wrong in terms of passing values
localhost:44338/Accounts/ResetPassword?vk={value}
it is looking for a parameter named vk but it should be validationKey
if you can change your SendEmail function to add validationKey instead of vk it will address your problem
or
in your ResetPassword(string validationKey) you can change the name to
ResetPassword(string vk)
I have a controller method called UserSignIn that's used to authenticate a user. The "Candidate" parameter is a model that contains fields including the contact's email address and password.
The model also contains fields "AgencyID" and "ContactID". These are used so that I know which database to connect to (AgencyID) and which contact record to get (ContactID). The user signing in is a contact at an agency.
[HttpPost()]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UserSignIn(Candidate can)
{
bool is_err = false;
string err = string.Empty;
Candidate c_signed_in = new Candidate();
// check data
if (string.IsNullOrEmpty(can.Email))
{
is_err = true;
err += "<li>Missing email address.</li>";
}
if (string.IsNullOrEmpty(can.AccountPassword))
{
is_err = true;
err += "<li>Missing password.</li>";
}
// get candidate
if (ModelState.IsValid && !is_err)
{
c_signed_in = await Repository.GetCandidate(can.AgencyID, 0, can.Email.ToLower(), can.AccountPassword, hostingEnv.WebRootPath);
if (c_signed_in.ContactID == 0)
{
is_err = true;
err += "<li>No account found. Check your credentials.</li>";
}
}
// check model state
if (!ModelState.IsValid || is_err)
{
Candidate c_current = await Repository.GetBlankCandidate(can, false);
c_current.IsModeSignIn = true;
if (is_err)
c_current.ErrsSignIn = "<ul class=\"text-danger\">" + err + "</ul>";
return View("Agency", c_current);
}
// create claims
var claims = new List<Claim>
{
//new Claim(ClaimTypes.Name, c_signed_in.FirstName + gFunc.SPACE + c_signed_in.FamilyName),
new Claim(ClaimTypes.Sid, c_signed_in.ContactID.ToString()),
new Claim(ClaimTypes.Email, c_signed_in.Email)
};
// create identity
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // cookie or local
// create principal
ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));
// sign-in
await HttpContext.SignInAsync(scheme: CookieAuthenticationDefaults.AuthenticationScheme, principal: principal);
// add to log
gFunc.AddLogEntry("SignIn Candidate: " + c_signed_in.FirstName + gFunc.SPACE + c_signed_in.FamilyName + " - " + c_signed_in.Email);
// fini
return RedirectToAction("Profile", new { agencyID = c_signed_in.AgencyID, contactID = c_signed_in.ContactID });
}
On success, this method redirects to a method called "Profile" that displays the user's profile.
[HttpGet]
[Authorize]
public async Task<ActionResult> Profile(int agencyID, int contactID)
{
Candidate can = await Repository.GetCandidate(agencyID, contactID, string.Empty, string.Empty, hostingEnv.WebRootPath);
if (can.ContactID == 0)
{
int id = agencyID;
return RedirectToAction("Agency", new { agencyID = id });
}
return View("Profile", can);
}
My URL is now "/Home/Profile?agencyID=5809&contactID=19492
However, I can now just change the contactID in the URL and now I'm on another user's profile without being authorized.
How do I avoid this? Obviously I can't include the password as a parameter in the Profile method because it would simply be visible in the URL. What approach should I be taking?
UPDATE - SOLVED
Thanks to all for your comments. Camilo Terevinto's answer solved my problem.
I added the info I needed to the claims in the UserSignIn method and removed the parameters in the Profile method, where I can retrieve the info I need from the active user. Now I can ensure that only the authorized user can reach the "Profile" controller method.
The only thing I had to change was the direct int cast. My compiler didn't like it, so I just change it to use a parse instead:
int agency_id = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
int contact_id = int.Parse(User.FindFirst(ClaimTypes.Sid).Value);
You can add agencyID and contactID to Claims:
new Claim(ClaimTypes.Sid, c_signed_in.ContactID.ToString()),
new Claim(ClaimTypes.Email, c_signed_in.Email),
new Claim(ClaimTypes.NameIdentifier,c_signed_in.agencyID.ToString())
In controller you can obtain it from logged user data:
[HttpGet]
[Authorize]
public async Task<ActionResult> Profile()
{
int agencyID = (int)User.FindFirst(ClaimTypes.NameIdentifier).Value
int contactID = (int) User.FindFirst(ClaimTypes.Sid).Value
Candidate can = await Repository.GetCandidate(agencyID, contactID, string.Empty, string.Empty, hostingEnv.WebRootPath);
if (can.ContactID == 0)
{
int id = agencyID;
return RedirectToAction("Agency", new { agencyID = id });
}
return View("Profile", can);
}
I am using MVC but I have a problem of dispose method is being called. When I am going to login, all the validation and login is working properly but when I am redirecting after a successful login, then in the case of return redirect to action, dispose method is being called.
What should I do?
Here is my controller:
public UserManager<ApplicationUser> UserManager { get; private set; }
public AdminController()
: this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
}
public AdminController(UserManager<ApplicationUser> userManager)
{
UserManager = userManager;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (Session["RoleID"] != null)
{
if (Convert.ToInt32(Session["RoleID"]) == Ansits2018.UTILITIES.Constants.Admin)
{
return RedirectToAction("Index", "Admin");
}
}
if (ModelState.IsValid)
{
var accRepo = new AccountRepository();
int UserID = 0;
UserID = accRepo.IsUserValid(model.UserName, model.Password);
if (UserID > 0)
{
var user = accRepo.GetUserByUsername(model.UserName);
Session["CompanyID"] = 1;
Session["UserID"] = UserID;
Session["Username"] = model.UserName;
Session["RoleID"] = user.RoleID;
Session["Name"] = user.Name;
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // ticket version
user.Username, // authenticated username
DateTime.Now, // issueDate
DateTime.Now.AddMinutes(60), // expiryDate
true, // true to persist across browser sessions
user.RoleID.ToString(), // can be used to store additional user data
FormsAuthentication.FormsCookiePath // the path for the cookie
);
// Encrypt the ticket using the machine key
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
// Add the cookie to the request to save it
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
Response.Cookies.Add(cookie);
if (!String.IsNullOrEmpty(returnUrl))
{
return RedirectToLocal(returnUrl);
}
if (user.RoleID == Ansits2018.UTILITIES.Constants.Admin)
{
return RedirectToAction("Index", "Admin");
}
}
else
{
TempData["Message"] = "Invalid username or password.";
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
protected override void Dispose(bool disposing)
{
if (disposing && UserManager != null)
{
UserManager.Dispose();
UserManager = null;
}
base.Dispose(disposing);
}
RedirectToAction (at least the form you're using) is constructed like this:
RedirectToAction(string actionName, string controllerName)
So return RedirectToAction("Index", "Home"); takes you to the Index action in the Home Controller, and return RedirectToAction("Index", "Admin"); takes you to the Index Action of the Admin Controller.
So if I'm reading your question correctly, you need to change the code to return RedirectToAction("Index", "Home");
Update (in light of comments):
If you are wanting to RedirectToAction("Index", "Admin"); then you'll need to debug your code to find out where the problem is. You can set breakpoints for the debug, or add these debug lines to your code:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (Session["RoleID"] != null)
{
if (Convert.ToInt32(Session["RoleID"]) == Ansits2018.UTILITIES.Constants.Admin)
{
return RedirectToAction("Index", "Admin");
}
}
if (ModelState.IsValid)
{
var accRepo = new AccountRepository();
int UserID = 0;
UserID = accRepo.IsUserValid(model.UserName, model.Password);
if (UserID > 0)
{
var user = accRepo.GetUserByUsername(model.UserName);
Session["CompanyID"] = 1;
Session["UserID"] = UserID;
Session["Username"] = model.UserName;
Session["RoleID"] = user.RoleID;
Session["Name"] = user.Name;
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // ticket version
user.Username, // authenticated username
DateTime.Now, // issueDate
DateTime.Now.AddMinutes(60), // expiryDate
true, // true to persist across browser sessions
user.RoleID.ToString(), // can be used to store additional user data
FormsAuthentication.FormsCookiePath // the path for the cookie
);
// Encrypt the ticket using the machine key
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
// Add the cookie to the request to save it
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
Response.Cookies.Add(cookie);
if (!String.IsNullOrEmpty(returnUrl))
{
System.Diagnostics.Debug.WriteLine("1 #####> RedirectToLocal (valid Login): " + returnUrl);
return RedirectToLocal(returnUrl);
}
if (user.RoleID == Ansits2018.UTILITIES.Constants.Admin)
{
return RedirectToAction("Index", "Admin");
}
}
else
{
System.Diagnostics.Debug.WriteLine("2 #####> Invalid Login Attempt - UserID > 0: " + model.UserName + " => " + model.Password));
TempData["Message"] = "Invalid username or password.";
return View(model);
}
}
System.Diagnostics.Debug.WriteLine("3 #####> ModelState Invalid: " + model.UserName + " => " + model.Password);
return View(model);
}
then click the cursor on the Login View Action (not the [HttpPost]), hit Debug in the menu and then Start Debugging. Wait for it to all settle down and then login and watch the Debug messages to see which one appears. That should give a good clue as to what's going wrong, and what you need to do.
I'm building a simple application where a user can edit their profile including adding/deleting a brand image. This seems to be working fine and is updating the database no problem, however when refreshing the page and retrieving the user details via Membership.GetUser() the result includes the old results and not those from the updated database.
Here is my MembershipUser GetUser override:
public override MembershipUser GetUser(string query, bool userIsOnline)
{
if (string.IsNullOrEmpty(query))
return null;
var db = (AccountUser)null;
// ...get data from db
if (query.Contains("#")){
db = _repository.GetByQuery(x => x.Email == query).FirstOrDefault();
}
else
{
string firstName = query;
string lastName = null;
if (query.Contains(" "))
{
string[] names = query.Split(null);
firstName = names[0];
lastName = names[1];
}
// ...get data from db
db = _repository.GetByQuery(x => x.FirstName == firstName && x.LastName == lastName).FirstOrDefault();
}
if (db == null)
return null;
ToMembershipUser user = new ToMembershipUser(
"AccountUserMembershipProvider",
db.FirstName + " " + db.LastName,
db.ID,
db.Email,
"",
"",
true,
false,
TimeStamp.ConvertToDateTime(db.CreatedAt),
DateTime.MinValue,
DateTime.MinValue,
DateTime.MinValue,
DateTime.MinValue);
// Fill additional properties
user.ID = db.ID;
user.Email = db.Email;
user.FirstName = db.FirstName;
user.LastName = db.LastName;
user.Password = db.Password;
user.MediaID = db.MediaID;
user.Media = db.Media;
user.Identity = db.Identity;
user.CreatedAt = db.CreatedAt;
user.UpdatedAt = db.UpdatedAt;
return user;
}
Note I am using a custom MembershipProvider and MembershipUser. Here is where I am calling that method:
public ActionResult Edit()
{
ToMembershipUser toUser = Membership.GetUser(User.Identity.Name, true) as ToMembershipUser;
Now when I do a separate query just under this line of code straight to the database, not invoking MembershipUser, I get the updated result which in turn updates the MembershipUser result?!
It seems it may be caching the results? Anyway around this? I hope this is clear enough. Thanks
Edit:
It appears that when I set a breakpoint just after :
// ...get data from db
db = _repository.GetByQuery(x => x.FirstName == firstName && x.LastName == lastName).FirstOrDefault();
'db' retrieves the outdated results though surely this is talking to the database? If need be I'll update with my repository pattern
I managed to find a workaround though I'm not happy with this solution, so if anyone can improve upon this please post.
I decided to manually update the MembershipUser instance manually each time I update the image. My controller now looks like this:
private static ToMembershipUser MembershipUser { get; set; }
// GET: Dashboard/AccountUsers/Edit
public ActionResult Edit()
{
if(MembershipUser == null)
MembershipUser = Membership.GetUser(User.Identity.Name, true) as ToMembershipUser;
}
[HttpPost]
[ValidateJsonAntiForgeryToken]
public JsonResult UploadMedia(IEnumerable<HttpPostedFileBase> files, int id)
{
var images = new MediaController().Upload(files);
if (images == null)
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return Json("File failed to upload.");
}
AccountUser accountUser = db.AccountUsers.Find(id);
db.Entry(accountUser).State = EntityState.Modified;
accountUser.UpdatedAt = TimeStamp.Now();
accountUser.MediaID = images[0];
db.SaveChanges();
MembershipUser.Media = accountUser.Media;
MembershipUser.MediaID = accountUser.MediaID;
return Json(new { result = images[0] });
}
[HttpPost]
[ValidateJsonAntiForgeryToken]
public JsonResult DeleteMedia(int id)
{
bool delete = new MediaController().Delete(id, 1);
if (!delete)
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return Json("Error. Could not delete file.");
}
MembershipUser.Media = null;
MembershipUser.MediaID = null;
return Json("Success");
}
I'm still very new to C# and would appreciate any help with my code.
I'm creating a user profile page and am getting the error "Nullable object must have a value" on "photo = (byte)user.Photo;" in the following code. I assume it's because I declared "photo = 0;" How do I add a value to it?
Update:
Here's the entire method
public static bool UserProfile(string username, out string userID, out string email, out byte photo)
{
using (MyDBContainer db = new MyDBContainer())
{
userID = "";
photo = 0;
email = "";
User user = (from u in db.Users
where u.UserID.Equals(username)
select u).FirstOrDefault();
if (user != null)
{
photo = (byte)user.Photo;
email = user.Email;
userID = user.UserID;
return true; // success!
}
else
{
return false;
}
}
}
I am assuming that you are getting error for this one...
if (user != null)
{
photo = (byte)user.Photo;
email = user.Email;
userID = user.UserID;
return true; // success!
}
else
{
return false;
}
If yes then just replace it with...
if (user != null)
{
photo = user.Photo== null ? null : (byte)user.Photo;
email = user.Email;
userID = user.UserID;
return true; // success!
}
else
{
return false;
}