ASP.NET MVC Custom Membership Provider "CreateUser" - asp.net-mvc

I've implemented some basic, custom membership provider for my ASP.NET MVC application so I thought that all validation will be done in my custom code.
Unfortunately when I'm trying to create new user by calling function:
Membership.CreateUser(user.UserName, user.Password, user.Email, null, null, true, Guid.NewGuid(), out status);
which should eventually throw an exception with all validation errors I'm getting a status like "InvalidUserName" or "InvalidPassword" instead... That means that my custom CreateUser function isn't call directly, it's used after some basic validation which I would wish to skip.
My CreateUser function (in my custom provider):
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
try
{
User user = new User();
user.UserKey = Guid.NewGuid();
user.UserName = username;
user.passwordSalt = string.Empty;
user.Password = this.TransformPassword(password, ref user.passwordSalt);
user.Email = email;
user.PasswordQuestion = passwordQuestion;
user.PasswordAnswer = passwordAnswer;
user.CreationDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
user.LastLoginDate = DateTime.MinValue;
user.LastPasswordChangeDate = DateTime.Now;
this._UsersRepository.SaveUser(user);
status = MembershipCreateStatus.Success;
return CreateMembershipFromInternalUser(user);
}
catch(RuleException ex)
{
throw ex;
}
}
Do you know how to enforce direct usage of custom CreateUser function !?
But I'm not using a default ASP.NET MVC project's AccountController...
Just take a look:
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(User user, string password_confirm, bool acceptsTerms)
{
if (!acceptsTerms)
ModelState.AddModelError("acceptsTerms", "Musisz zaakceptować regulamin");
if (ModelState.IsValid)
{
try
{
MembershipCreateStatus status = new MembershipCreateStatus();
Membership.CreateUser(user.UserName, user.Password, user.Email, null, null, true, Guid.NewGuid(), out status);
}
catch (RuleException ex){
ex.CopyToModelState(ModelState,"user");
}
}
return View();
}
The point is that I'm getting a status instead of RuleException ex when user.UserName or user.Password is empty. My custom RuleException ex would give me back such informations as well. Where a status value is assigned right now !? Bacouse it's not done in my implementation of CreateUser...

I am using this (MVC3) and I have no issues:
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
MembershipCreateStatus status;
Membership.Provider.CreateUser(model.UserName, model.Password, model.Email, "", "", true, Guid.NewGuid(), out status);
if (status == MembershipCreateStatus.Success)
{
FormsService.SignIn(model.UserName, false);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", AccountValidation.ErrorCodeToString(status));
}
}
// If we got this far, something failed, redisplay form
ViewBag.PasswordLength = MembershipService.MinPasswordLength;
return View(model);
}

Using ILSpy to view the static method Membership.CreateUser, you will find it performs validation on
Username (trim whitespace + not null, not empty)
Password (trim whitespace + not null, not empty, length checks)
Email (trim whitespace)
Password Question (trim whitespace + not empty)
Password Answer (trim whitespace + not empty)
It then calls the custom provider.
The key here is to not call the static method CreateUser
Membership.CreateUser(...)
Rather, call the custom provider directly by using:
Membership.Provider.CreateUser(...)
Tested and verified working, as of .NET 4
While my site recommends having a password, we support openid. So forcing a user to have a password just seemed counter to what openid is good for.

Complete rewrite
The ASP.NET Membership system always does some initial validation of the inputs when APIs such as CreateUser are called.
I don't know of any way around this aside from not going directly to the ASP.NET membership APIs.

Related

ASP.NET MVC 5 - let admin change other users password. Password changed in database but can't login

I'm told to make admin have a functionality to change other users password without knowing their original password. I wrote a code that changes and saves password successfully in database, but when I try to login as that user I can't.
UsersController:
public ActionResult ChangePassword()
{
return View();
}
[HttpPost]
public ActionResult ChangePassword(int id, ViewModels.ChangePasswordViewModel model)
{
if (!SessionControlService.CheckIsLoginStillTrue(_loginsService, HttpContext))
return RedirectToAction("Login", "Account");
if (!User.IsInAnyRoles("Admin", "PropertyManager"))
return RedirectToAction("Error", "Errors",
new { error = Facility.Web.Resources.Resources.ErrorNotHavePermission });
var user = _userService.GetUser(id);
if (user == null)
return RedirectToAction("Error", "Errors",
new { error = Facility.Web.Resources.Resources.ErrorURLNotExist });
user.Password = model.NewPassword;
_userService.UpdateUser(user);
return RedirectToAction("Details", new { id = id });
}
Why can't I use the changed password which is saved in the database to login?
How can I make this work?
In ASP.NET MVC5, password is hashed... you cannot save a plaintext password like that.
You need to use these two methods:
var manager = new ApplicationUserManager(...);
var token = manager.GeneratePasswordResetToken(userId)
manager.ResetPassword(userId, token, newPassword)
You could also try ApplicationUserManager.UpdatePassword(...), or RemovePassword(...) and AddPassword(...)
ApplicationUserManager is normally in IdentityConfig.cs

MVC Email Confirmation (Sequence contains more than one element)

I'm currently trying to add email confirmation to my website and i'm experiencing a few problems.
I can succesfully register an account, upon registration the confirmationToken gets placed in my database and a email gets send with the query string link: http://www.example.com/RegistrationConfirmation?9ZPwZZrO-UmdpVpxXWjmRw when going to this link the controller action RegistrationConfirmation gets called and the method ConfirmAccount does a query to see if we can find a user with the confirmation token that was passed in the url.
When debugging I get the error "Sequence contains more than one element" on this line: Account user = context.Accounts.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
I'm not sure what's going wrong cause the token is unique and there are no duplicate tokens in the database.
Register HttpPost:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterModel model)
{
string confirmationToken = CreateToken();
Account account = new Account(model.Username, model.Password, model.FirstName, model.LastName, model.Email, false, confirmationToken);
if (DatabaseHandler.isUsernameDuplicate(account.Username))
{
// is duplicate // provide notification
}
else
{
Session["accountID"] = Repository.InsertAccount(new Account(model.Username, model.Password, model.FirstName, model.LastName, model.Email,false, confirmationToken));
// Email Logic
try
{
await client.SendMailAsync(message);
}
catch (Exception e)
{
ModelState.AddModelError("", "Problem sending email: " + e.Message);
}
return View("ConfirmEmail");
}
return View();
}
RegistrationConfirmation HttpGet:
[HttpGet]
[AllowAnonymous]
public ActionResult RegisterConfirmation(string Id)
{
if (ConfirmAccount(Id))
{
return RedirectToAction("ConfirmationSuccess");
}
return RedirectToAction("ConfirmationFailure");
}
ConfirmAccount method:
private bool ConfirmAccount(string confirmationToken)
{
RecipeDbContext context = new RecipeDbContext();
Account user = context.Accounts.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
if (user != null)
{
user.IsConfirmed = true;
DbSet<Account> dbSet = context.Set<Account>();
dbSet.Attach(user);
context.Entry(user).State = EntityState.Modified;
context.SaveChanges();
return true;
}
return false;
}
Remove SingleorDefault() and check the result.. you will get to know its not duplicate and if you want to avoid this error then use FirstorDefault().
Turned out nothing was wrong with the query itself, the RegisterConfirmation HttpGet did not seem to get the confirmToken value so I added "Id = Request.QueryString.ToString();" and everything is working perfect now :)

Redirection based on user role upon login

I am trying to redirect user to page based on their role. Below is the login code in a controller:
[HttpPost]
public ActionResult Login(User model)
{
// Lets first check if the Model is valid or not
if (ModelState.IsValid)
{
using (AuthenticationAppEntities1 entities = new AuthenticationAppEntities1())
{
string username = model.Username;
string password = model.Password;
// Now if our password was enctypted or hashed we would have done the
// same operation on the user entered password here, But for now
// since the password is in plain text lets just authenticate directly
bool userValid = entities.Users.Any(user => user.Username == username && user.Password == password);
// User found in the databases
if (userValid)
{
FormsAuthentication.SetAuthCookie(username, false);
if (Roles.IsUserInRole(model.Roles, "admin"))
{
return RedirectToAction("Home", "Authentication");
}
else
{
return RedirectToAction("HomeAdmin", "Authentication");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
As you can see in the code, I redirect users based on their roles, using a if. However, model.Roles contain a null value as the controller is getting only the username and password from the login page. Any simple idea how I get the role of the authenticated user? I thought of searching it through the username, but am not sure if this is the best solution.
You can get the User's roles and then compare it with 'admin' and then redirect accordingly.
Honestly, I'm not even sure how you're getting that to run in the first place. The method signature for Roles.IsUserInRole is:
Roles.IsUserInRole(string username, string role);
Passing model.Roles is neither a string, nor a username, if it was a string.

Asp.net Identity - token is not matching encoding issue?

I am trying to use asp.net identity for authentication, I am having some issues with encoding/decoding.
User clicks on forgot password link, so we call out:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[PassModelStateToTempData]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
logger.Info("reset_password attempting for {0}", model.Email);
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
this.Flash("Please check your email, we have sent you instructions on how to reset your password");
return RedirectToAction("ForgotPassword");
}
string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
logger.Debug("forgot_password code {0}", code);
var callbackUrl = Url.Action("ResetPassword", "Session", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
messagingService.ResetPassword(user.Email, callbackUrl);
this.Flash("Please check your email, we have sent you instructions on how to reset your password");
logger.Debug("remind_me successfully send out email to {0} {1}", model.Email, callbackUrl);
return RedirectToAction("ForgotPassword");
}
logger.Info("reset_password failed for {0}", model.Email);
// If we got this far, something failed, redisplay form
return RedirectToAction("ForgotPassword");
}
User gets email then clicks link so we run:
[HttpGet]
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
if (code == null)
{
this.Flash("Invalid login token, please enter your email address again");
return RedirectToAction("ForgotPassword");
}
var vm = new ResetPasswordViewModel
{
Code = code
};
return View(vm);
}
We pass on token into view - we ask for email and password, then user hits post and we run:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return RedirectToAction("ResetPassword");
}
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{
logger.Info("reset_password user not found [{0}]", model.Email);
// Don't reveal that the user does not exist
return RedirectToAction("ResetPasswordConfirmation", "Session");
}
var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Session");
}
AddErrors(result);
return RedirectToAction("ResetPassword", new { code = model.Code });
}
For some reason tokens seem to not match, here are an example of the token I am getting - why the case difference?
Token:
2015-10-14 13:06:52.7545|DEBUG|Controllers.Application|forgot_password code BoUZZ9OS7rEkKMkEJzerWdds4dZLHFTHO/EkjQC2Zr8YJvCyjsXUKBRLZk8jmAqhjyxOzgqOLdJ8P/ji8y+om2ne7bcsLICzcdLSHzrP6BNEr1/+HKvHcYan+JzAX7Ifpgq7casmMj4f9esAdxejLA==
Notice the case difference:
2015-10-14 13:07:29.7164|INFO|Controllers.Application|reset_password attempting for my.email#gmail.com with token: bouzz9os7rekkmkejzerwdds4dzlhftho/ekjqc2zr8yjvcyjsxukbrlzk8jmaqhjyxozgqoldj8p/ji8y+om2ne7bcsliczcdlshzrp6bner1/+hkvhcyan+jzax7ifpgq7casmmj4f9esadxejla== -> Invalid token.
Your MVC routing is set up to generate lowercase URLs:
routes.LowercaseUrls = true;
This means that your codes are also being converted to lowercase. Possible solutions are:
Turn off LowercaseUrls if you can (or want)
Use MVC attribute routing, though this can be quite a switch.
The simplest option for you may be to simply create the URL yourself:
//Generate the URL without the code parameter
var callbackUrl = Url.Action(
"ResetPassword",
"Session",
new { userId = user.Id },
protocol: Request.Url.Scheme);
//Manually add the code, remembering to encode it
callbackUrl = callbackUrl + "&code=" HttpUtility.UrlEncode(code);

Custom login with mvc messing up with the login partial view

So I've set up a custom login in my MVC application which appears to work...
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(User model, string returnUrl)
{
if (ModelState.IsValid)
{
if (model.Login())
{
FormsAuthentication.SetAuthCookie(model.Username, true);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/") && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "Invalid email address or password.");
}
}
// If execution got this far, something failed, redisplay form
return View(model);
}
public class User
{
public bool Login()
{
var user = db.Users.FirstOrDefault(u => u.EmailAddress == EmailAddress);
if (user == null)
{
throw new ValidationException("User not found.");
}
else
{
// validates whether or not the password on the user record
// that was retrieved by the query matches the password entered at login
return Hashing.ValidatePassword(Password, user.Password);
}
}
}
Unfortunately, there's some conflict between it and the default _LoginPartial.cshtml View (which looks like below):
#model LoganMVC.Models.User
#if (Request.IsAuthenticated) {
<text>
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", new { User.Identity.Name })!
#using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm" })) {
#Html.AntiForgeryToken()
Log off
}
</text>
} else {
<ul>
<li>#Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
<li>#Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
</ul>
}
The ArgumentNullException highlights this line...
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", new { User.Identity.Name })!
Saying...
Value cannot be null or empty.
It's clear that the value that cannot be null or empty is User.Identity.Name, but what isn't clear is WHY IsAuthenticated is true to begin with. This is Forms Authentication and, as far as the software is aware (because I killed the debug server, closed the solution and started debugging fresh again), the application has never run before.
Okay I've been bashing my head against various hard surfaces trying to get this one figured out and eventually did.
After looking at the errors I was initially getting:
Value cannot be null or empty.
And then later
The provided identity of type 'System.Web.Security.FormsIdentity' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.
Were fairly simple fixes.
First off, I had initially set my login cookie to persist. This means that it wouldn't be deleted when the browser is closed and would not expire either. While this did not cause the Value cannot be null or empty error, it was contributory in that the site was finding it and assuming a user had authenticated.
FIX
To fix this, I simply cleared cookies from my browser set the cookie's persist to false. This allowed me to focus more on the actual problem.
Value cannot be null or empty
This problem was actually not as complicated as I thought. When I started taking a closer look at things, I noticed that there was an error in the following code:
[HttpPost]
public ActionResult Login(User model, string returnUrl)
{
if (ModelState.IsValid)
{
if (model.Login())
{
FormsAuthentication.SetAuthCookie(model.Username, true);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/") && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "Invalid email address or password.");
}
}
// If execution got this far, something failed, redisplay form
return View(model);
}
This post captures an instance of my User model from the form (which is the login form so only EmailAddress and Password values are included). It also uses the Username value from this model (which is null) to set the Authentication Cookie.
I changed my .Login() method to a function that returns a User if the email address is found and the stored hash matches a hash of the password entered for the login.
The code, thus, changed to the following (this is only the key point of the change)
// Account.Login Post
if (ModelState.IsValid)
{
var user = model.Login(model.EmailAddress);
if (user == null)
{
ModelState.AddModelError("", "Invalid email address or password.");
}
FormsAuthentication.SetAuthCookie(user.Username, true);
// create session variables n such
}
// User.Login
public User Login(string EmailAddress
{
var user = dbContext.Users.FirstOrDefault(u => u.EmailAddress == Email);
if (user == null)
throw new Exception("User not found.");
if (Hashing.ValidatePassword(Password, user.Password))
return user;
return null; // if we got here, something went wrong
}
Once I'd fixed this, I got another problem later. Some issue to do with the Anti-Forgery system.
While not a solution, I managed a workaround and commented out the following line from my _LoginPartial View:
#Html.AntiForgeryToken()
I had concerns about security but I figured that Forms Auth on a basic asp.net website with a hashed and salted password has been sufficient for quite a number of years, so I shouldn't have any issues with it here.
You probably miss something. In your controller Account you have an Action:
public ActionResult Manage(string name) // I presume name
{
....
}
Then your line should be:
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", new { name=User.Identity.Name })!
The anonymous object which is routeValues
It's n object that contains the parameters for a route. The parameters are retrieved through reflection by examining the properties of the object. The object is typically created by using object initializer syntax.(Definition taken from MSDN) And it should be well formatted.
I hope it will solve the problem otherwise you have to check if the User.Identity is different of null.
I have noticed also somthing else in this line:
var user = db.Users.FirstOrDefault(u => u.EmailAddress == EmailAddress);
Where comes from the EmailAddress after ==?
You should provide a parameter to you Login method to replace your second EmailAddress.
public bool Login(string emailAddress)
{
var user = db.Users.FirstOrDefault(u => u.EmailAddress == emailAddress);
if (user == null)
{
throw new ValidationException("User not found.");
}
else
{
// validates whether or not the password on the user record
// that was retrieved by the query matches the password entered at login
return Hashing.ValidatePassword(Password, user.Password);
}

Resources