Trying to get password reset functionality in place. This will be for a user who has not and cannot log in to the system. I think I'm close but it doesn't feel right:
I have a ResetPassword method/view...it simply asks for the user's email address, does not confirm an account to the user but if one exists, sends email with link+token. That all works fine.
The next part is where my questions are....I receive the password token with this method (via the user's email link being clicked):
[HttpGet]
public ActionResult ReceiveResetToken(string token)
{
try
{
if (!String.IsNullOrEmpty(token))
{
var username = (from u in db.Users
where u.Userid == WebSecurity.GetUserIdFromPasswordResetToken(token)
select u.Email).ToString();
if (!String.IsNullOrEmpty(username))
{
WebSecurity.ConfirmAccount(token);
}
}
RedirectToAction("Index", "Home");
}
catch (Exception)
{
throw;
}
}
I'm missing something obvious here. The method isn't complete because I keep rethinking it...get the username, confirm the account, somehow log them in without knowing what their password is, redirect to change password page? Doesn't feel right.
So then I thought maybe pass along the hidden username with a ViewBag to the change dialogue...doesn't feel right either. I'd like a more elegant approach, receive the token, ask the username for the new password, update db and login. What is the pattern for receiving a password reset token?
EDIT -------
So as I am continuing to search for answers, I came across this little gem. Apparently there is a WebSecurity.ResetPassword method that accepts the token and a new password. This feels like the right path, no need to worry about logins, just change it and redirect to login...I'll finish up the code and post a solution as this seems to be a popular and often unanswered question on SO.
If anyone could confirm that I'm on the right path or post any thoughts on adding elegance to the pattern that'd be cool
It's a right path !
for me,
User give his email and i send him a token who is generate an GUID and i have passwordResetTokenDate who take a date when user asked the reset. (token is valid 48hours)
in email, there is a link with token and i give him a token, if when he click and something is wrong, he can copy pasted the token in textbox or re-clicking on the link
when he click on the link, i check the token and the date and passwordResetTokenDate if all is right, there is two textbox and user enter 2 times his new password.
when he save his password, i logged him.
WebSecurity.ResetPassword do the job !
here an example : (i have a custom websecurity with custom provider)
[AllowAnonymous]
public ActionResult ForgotMyPassword(string confirmation, string username)
{
username = HttpUtility.UrlDecode(username);
ViewBag.Succeed = false;
SetPasswordViewModel Fmp = new SetPasswordViewModel(username,confirmation);
return View(Fmp);
}
//
// POST: /Account/ForgotMyPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ForgotMyPassword(SetPasswordViewModel model)
{
ViewBag.Succeed = false;
if (ModelState.isValid)
{
ViewBag.Succeed = WebSecurity.ResetPassword(model.UserName, model.PasswordResetToken, model.Password.NewPassword);
}
if (!ViewBag.Succeed)
{
ModelState.AddModelError("","something"); //something
}
return View(model);
}
This is how it should work (as implemented in ASP Security Kit)
User clicks on forgot password link (which opens /account/forgot for example)
On this page, you ask user for his userName (which could be his email).
You check whether that user exists. If yes, you generate a reset token, saving it in the database for that username and send out an email to that user with a link (http://yourdomain.com/account/confirm/[tokenHere])
You display user a message something like "if you have an account with this username, you will receive an email with instructions to reset your password shortly." but you don't login user on this page because you just asked him for his username!
User receives the email, clicks on the link and the reset password page opens (/account/confirm/[tokenHere])
On this page, user needs to fill password and confirm password fields. Once done you will redirect user to login page (you may argue that you can directly sign in user once he resets his password; but redirecting to login seems to be the standard practice followed on most sites.)
Answering my own question in case it helps anyone.
Provide a form asking for email address to send password reset link
Use WebSecurity.GeneratePasswordResetToken(email, 1440) to generate token
Send email to user with link pointing to a token receiving method
Write an HttpGet method to receive the token and display newPassword form
The form posts the token and the new password model to a method that uses WebSecurity.ResetPassword(token, newPassword)
redirect to login
Haven't written it all out yet but I think this is the way to do it properly
Related
Problem
I am trying to handle the 'Reset password' user-flow in my application. Upon clicking the 'Forgot Password' link, the OnRemoteFailure OpenId event is successfully triggered, successfully redirecting to the specified url 'Home/ResetPassword', but instead or redirecting to the ADB2C reset password screen, it's redirecting back to the sign-in/sign-up page.
Background
The sign-up/sign-in policy works successfully but as per Microsoft docs: https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-policies:
" A sign-up or sign-in user flow with local accounts includes a Forgot password? link on the first page of the experience. Clicking this link doesn't automatically trigger a password reset user flow.
Instead, the error code AADB2C90118 is returned to your application. Your application needs to handle this error code by running a specific user flow that resets the password. To see an example, take a look at a simple ASP.NET sample that demonstrates the linking of user flows. "
Active Directory B2C Settings
UserFlows
Code
OpenIdEvent
protected virtual Task OnRemoteFailure(RemoteFailureContext context)
{
context.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect("/Home/ResetPassword");
}
else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
{
context.Response.Redirect("/");
}
else
{
context.Response.Redirect("/Home/Error?message=" + WebUtility.UrlEncode(context.Failure.Message));
}
return Task.FromResult(0);
}
HomeController
public IActionResult ResetPassword()
{
var redirectUrl = Url.Action(nameof(HomeController.Index), "Home");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
properties.Items[AzureADB2COptionsExtended.PolicyAuthenticationProperty] = _adb2cOptions.ResetPasswordPolicyId;
return Challenge(properties, AzureADB2CDefaults.AuthenticationScheme);
}
A lot of examples I found use OWIN... There is very limited documentation on ASP.Net Core 2.2 w/ ADB2C.
The Sign-up-sign-in policy now has built-in support for password resets without a second "password-reset" user flow. It is quite confusing with all the documentation and samples out there but this is the latest docs and it works for us!
https://learn.microsoft.com/en-us/azure/active-directory-b2c/force-password-reset?pivots=b2c-user-flow
Normally, the ResetPassword flow you configured in your appsettings.json is called automatically when using the Microsoft.Identity.Web package. In your case B2C_1_SSPR. That means you must define a custom user flow with this Id.
(I guess SSPR = self-service password reset)
The only thing you need in this default case is call the following in your Startup:
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "ActiveDirectoryB2C");
This works like a charm.
However, you decided to deal with all this stuff by yourself and not to use the Microsft.Identity.Web library for all processing.
In this case you are able to handle Password-reset by yourself.
But let's have a look at the Microsft.Identity.Web. They integrated an AccountController that handles the processing of the B2C custom flows (in this case the ResetPassword action).
The exception handling for AADB2C90118 can be found in the source code of the Identity package:
if (isOidcProtocolException && message.Contains(ErrorCodes.B2CForgottenPassword))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect($"{context.Request.PathBase}/MicrosoftIdentity/Account/ResetPassword/{SchemeName}");
}
Answer that was posted in the question:
For anyone else having the same or similar issue, make sure to keep an eye out on the OpenIdConnectEvents. We have been experimenting with ADB2C/OpenID and had test code. This code was obviously invalid.
protected virtual Task OnRedirectToIdentityProvider(RedirectContext context)
{
string policy = "";
context.Properties.Items.TryGetValue(AzureADB2COptionsExtended.PolicyAuthenticationProperty, out policy);
if (!string.IsNullOrEmpty(policy) && !policy.ToLower().Equals(_adb2cOptions.DefaultPolicy.ToLower()))
{
context.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower().Replace(_adb2cOptions.DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
See the latest "Self-Service Password Reset" policy (custom or non-custom) from Microsoft docs.
From the doc:
"The new password reset experience is now part of the sign-up or sign-in policy. When the user selects the Forgot your password? link, they are immediately sent to the Forgot Password experience. Your application no longer needs to handle the AADB2C90118 error code, and you don't need a separate policy for password reset."
I'm working on a solution using ASPNET Core 2.1 with Individual autentication. I was able to implement a seed class which creates the Identity roles, the admin user and assigns a role to the admin user when the host runs for the first time. After the first run, I check the database and everything is working fine. I don't like the 'Hello, userdummy#domain.com' welcome format message so I intend to change this in the future switching from email format to something more friendly as a username. Because of that, I use a different username from email address. When I assign this different username, login fails, but if I switch back to email-email for username and email fields, login works. I want a different username from email address. Any ideas about why is that happening?
This is the piece of code in my seed class which creates a new user:
if (!_dbContext.Users.Any()) // if users table is empty
{
// instantiate a user-store class
var _userStore = new UserStore<IdentityUser>(_dbContext);
// create a new user object with a different username
var admin = new IdentityUser
{
Email = "admin#admin.com",
UserName = "Administrador" // it makes login to fail
};
try
{
// ask the store-guy to create a new admin user with the given ridiculous password :D
var result = await _userStore.CreateAsync(admin,"123456");
}
catch (System.Exception ex)
{
_logger.Error(ex, "Sorry. Something went wrong here.");
}
}
If I change the username to have the same string than the email address, I can login with no problems.
I don't want to login using username. I want to login using email address but show a different string, like a name, in welcome message.
The default convention is for an IdentityUser to login against the 'UserName` field.
You can allow an email address as a username by turning off "AllowOnlyAlphanumericUserNames"
UserManager.UserValidator = new UserValidator<TUser>(UserManager) { AllowOnlyAlphanumericUserNames = false }
See answer and usage here
As a final solution to my question, guided by the help of you guys, would be either change the validation rules for registration and sign-in forms to point to UserName (the real field being checked against User table) or try to create a claim and use it as the name to be shown in welcome and other messages. This last one would be a valid, simple and harmless workaround. Thank you all.
Please see Update Below:
I am using ASP.NET SQL Membership Provider.
So far I am able to allow users to change their password but only if they are authenticated or logged into the application.
What I really need is for users to be able to get a link in an email. They can click this link and reset their password.
Example: Lets say a user forgets his or her password, they can visit a page which they can either enter security question and answer; or their email address on file. They will then get an email with a link to reset their password.
All I have so far is this: Which allows only authenticated users to reset their passwords:
I do not want to use the Recovery Control which generates a password.
public void ChangePassword_OnClick(object sender, EventArgs args)
{
MembershipUser user = Membership.GetUser(User.Identity.IsAuthenticated);
try
{
if (user.ChangePassword(OldPasswordTextbox.Text, PasswordTextbox.Text))
{
Msg.Text = "Password changed.";
}
else
{
Msg.Text = "Password change failed. Please re-enter your values and try again.";
}
}
catch (Exception e)
{
Msg.Text = "An exception occurred: " + Server.HtmlEncode(e.Message) + ".
try again.";
}
}
I can create the store procedure and the email using a String Builder but I do not know how to get the un-authenticated user to change password. Is there a way for the user to be Authenticated when they click the link. I am not sure how to even ask this.
Thanks for reading:
Update:
Okay I managed to get the password to Reset using this code:
protected void btnResetPassword_Click(object sender, EventArgs e)
{
string username = "ApplePie12";
MembershipUser currentUser = Membership.GetUser(username);
currentUser.ChangePassword(currentUser.ResetPassword(), txtResetPassword.Text);
}
Here is my plan:
Make this page public so that it is access by Un-Authenticated Users but only via email link.
Create a Stored Procedure that verifies a user Exists either by the UserName they enter or by the Security Question/Answer.
If they exists, they are sent a link containing a token/GUID
Lastly when they click the link they will land on this page asking them to change password. *The Link Expires as soon as it is used.
My only question is: Doing all of the above requires turning off using security Question/Answer in the Web Config file.
I really would love to have the Security question as an option for the user to either verify by email or security question. If this is not possible, I'll have to create some kind of account number or userid (not membership user id) as an alternative.
My answer is not specific to Membership Provider, but hopefully will point you in the right direction. Typically the way to approach this is to generate a very long random string, called a token. You send them a link that includes this token as a parameter, something like:
http://foo.bar/reset?token=asldkfj209jfpkjsaofiu029j3rjs-09djf09j1pjkfjsodifu091jkjslkhfao
Inside your application you keep track of tokens you have generated. If you receive a request containing that token, you authenticate it as if it was that user.
A couple notes:
The token generated should be random and effectively unguessable in a short period of time.
The token should only work for a short period of time after being generated, ideally shorter than the time required to guess it.
The token should only be usable once. Once a user has changed their password using it, remove it from the system.
Chris has given definitely the correct solution.
You can use the sql table for token management. the token may be UserId or Email that are unique. the link used for reset email like http://test.com/reset?id=sfksdfh-24204_23h7823.
The id in the url is encrypted Userid or Email as you like.
Fetch the detail from table on the basis of id in Url. if id contain in database. then reset the password for user. and remove that token from DB.
I just wanted to ask for help to get my scenario work? I want to get the UserName using a PasswordResetToken.
This is my scenario.
I have a forgot password feature in my website that would send a passwordresettoken email a change password to the user.
I wanted to send just the passwordresettoken string only.
When the user clicks the link. I will just query the request["token"] to get the username and and then will allow the user to change password and autologin.
this is my code below:
public ActionResult ChangePassword()
{
ChangePasswordModel model = new ChangePasswordModel();
string token=string.Empty;
try
{
token = Request["token"].ToString();
int userId = WebSecurity.GetUserIdFromPasswordResetToken(token);
if (userId > 0)
{
//Get the user object by (userid)
//???????????????????
//???????????????????
}
else
{
throw new Exception("The change password token has expired. Please go to login page and click forgot password again.");
}
}
catch
{
model.HasError = true;
ModelState.AddModelError("", "The change password token has expired. Please go to login page and click forgot password again.");
}
return View(model);
}
Thank you in advance.
Look at the remark at the end of this article: WebSecurity.GeneratePasswordResetToken Method.
I'll copy the relevant part for your convenience:
If users have forgotten their password, they can request a new one. To
provide a new password, do the following:
Create a password-reset page that has a field where users can enter their email address.
When a user has entered his or her email address in the password-reset page, verify that the email address represents a valid
user. If it does, generate a password reset token by calling the
GeneratePasswordResetToken(String, Int32) method.
Create a hyperlink that points to a confirmation page in your site and that includes the token as a query-string parameter in the link's
URL.
Send the link to a user in an email message. When the user receives the email message, he or she can click the link to invoke the
confirmation page.
Create a confirmation page that extracts the token from the URL parameter and that lets the user enter a new password.
When the user submits the new password, call the ResetPassword(String, String) method and pass the password reset token
and the new password. If the token is valid, the password will be
reset. If the token is not valid (for example, it has expired),
display an error message.
Highlighting is mine. Basically you do not need the user name. The framework does all the heavy lifting for you.
Addressing your comment, I would not recommend automatically logging the user in. It's a good practice for them to log manually to check that this password changing thingie has actually worked, and not to discover that it did not only next time around.
Anyway, you can do this:
SimpleMembershipProvider provider = (SimpleMembershipProvider)Membership.Provider;
string username = provider.GetUserNameFromId(userId);
Reference: GetUserNameFromId.
I think the WebSecurity.GetUserIdFromPasswordResetToken(string token) method do what you want.
More info here.
Update:
Sorry but I didn't saw that you were already using that method... So if you want get the username and you are using code first migrations of Entity Framework, you can get the username with the following LINQ expression:
string username = yourDbContext.UserProfiles.FirstOrDefault(up=>up.UserId == userId).Username;
I have a site where I allow some users to proxy in as an other user. When they do, they should see the entire site as if they where the user they proxy in as. I do this by changing the current user object
internal static void SetProxyUser(int userID)
{
HttpContext.Current.User = GetGenericPrincipal(userID);
}
This code works fine for me.
On the site, to proxy in, the user selects a value in a dropdown that I render in my _layout file as such, so that it appears on all pages.
#Html.Action("SetProxyUsers", "Home")
The SetProxyUsers view looks like this:
#using (#Html.BeginForm("SetProxyUsers", "Home")) {
#Html.DropDownList("ddlProxyUser", (SelectList)ViewBag.ProxyUsers_SelectList, new { onchange = "this.form.submit();" })
}
The controller actions for this looks like this
[HttpGet]
public ActionResult SetProxyUsers()
{
ViewBag.ProxyUsers_SelectList = GetAvailableProxyUsers(originalUserID);
return PartialView();
}
[HttpPost]
public ActionResult SetProxyUsers(FormCollection formCollection)
{
int id = int.Parse(formCollection["ddlProxyUser"]);
RolesHelper.SetProxyUser(id);
ViewBag.ProxyUsers_SelectList = GetAvailableProxyUsers(originalUserID);
return Redirect(Request.UrlReferrer.ToString());
}
All this works (except for the originalUserID variable, which I put in here to symbolize what I want done next.
My problem is that the values in the dropdown list are based on the logged in user. So, when I change user using the proxy, I also change the values in the proxy dropdown list (to either disappear if the "new" user isn't allowed to proxy, or to show the "new" user's list of available proxy users).
I need to have this selectlist stay unchanged. How do I go about storing the id of the original user? I could store it in a session variable, but I don't want to mess with potential time out issues, so that's a last resort.
Please help, and let me know if there is anything unclear with the question.
Update
I didn't realize that the HttpContext is set for each post. I haven't really worked with this kind of stuff before and for some reason assumed I was setting the values for the entire session (stupid, I know). However, I'm using windows authentication. How can I change the user on a more permanent basis (as long as the browser is open)? I assume I can't use FormAuthentication cookies since I'm using windows as my authentication mode, right?
Instead of faking the authentication, why not make it real? On a site that I work on we let admins impersonate other users by setting the authentication cookie for the user to be impersonated. Then the original user id is stored in session so if they ever log out from the impersonated users account, they are actually automatically logged back in to their original account.
Edit:
Here's a code sample of how I do impersonation:
[Authorize] //I use a custom authorize attribute; just make sure this is secured to only certain users.
public ActionResult Impersonate(string email) {
var user = YourMembershipProvider.GetUser(email);
if (user != null) {
//Store the currently logged in username in session so they can be logged back in if they log out from impersonating the user.
UserService.SetImpersonateCache(WebsiteUser.Email, user.Email);
FormsAuthentication.SetAuthCookie(user.Email, false);
}
return new RedirectResult("~/");
}
Simple as that! It's been working great. The only tricky piece is storing the session data (which certainly isn't required, it was just a nice feature to offer to my users so they wouldn't have to log back in as themselves all the time). The session key that I am using is:
string.Format("Impersonation.{0}", username)
Where username is the name of the user being impersonated (the value for that session key is the username of the original/admin user). This is important because then when the log out occurs I can say, "Hey, are there any impersonation keys for you? Because if so, I am going to log you in as that user stored in session. If not, I'll just log you out".
Here's an example of the LogOff method:
[Authorize]
public ActionResult LogOff() {
//Get from session the name of the original user that was logged in and started impersonating the current user.
var originalLoggedInUser = UserService.GetImpersonateCache(WebsiteUser.Email);
if (string.IsNullOrEmpty(originalLoggedInUser)) {
FormsAuthentication.SignOut();
} else {
FormsAuthentication.SetAuthCookie(originalLoggedInUser, false);
}
return RedirectToAction("Index", "Home");
}
I used the mvc example in the comments on this article http://www.codeproject.com/Articles/43724/ASP-NET-Forms-authentication-user-impersonation to
It uses FormsAuthentication.SetAuthCookie() to just change the current authorized cookie and also store the impersonated user identity in a cookie. This way it can easily re-authenticate you back to your original user.
I got it working very quickly. Use it to allow admin to login as anyone else.