Form Authentication : Roles (MVC 4) C# - asp.net-mvc-4

I am trying to implement Forms Authentication in my Application. I various examples and looked at the samples and questions provided in this forum and ASP.net MVC but I just can't get it to work.
I manage to authenticate my user but the roles does not seem to work :-(
I have setup my Web.Config as follow :
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>
In my Controller I set the Index page to AllowAnonymous and then check in there if the user is authenticated. If not then redirect to the login page..
[AllowAnonymous]
public ActionResult Index(string sortOrder, string searchString,string currentFilter, int? page)
{
if (!Request.IsAuthenticated)
{
return RedirectToAction("Login", "Account");
}
//Find all the employees
var employees = from s in db.Employees
select s;
//Pass employees to the view (All works fine)
return View(employees.ToPagedList(pageNumber, pageSize));
}
This all is working 100%
My Login code looks like this :
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(User user, string returnUrl)
{
var myUser = db.Users.Where(b => b.UserName == user.UserName).FirstOrDefault();
if(myUser != null)
{
if(myUser.Password==user.Password)
{
//These session values are just for demo purpose to show the user details on master page
//Session["User"] = user;
ICollection<UserAccessLevel> levels = db.UserAccessLevels.Where(b => b.UserId == myUser.UserId).ToList();
//Session["levels"] = levels;
//Let us now set the authentication cookie so that we can use that later.
FormsAuthentication.SetAuthCookie(user.UserName, false);
return RedirectToAction("Index","Employee");
}
}
ViewBag.Message = "Invalid User name or Password.";
return View(user);
}
I also have the following code in the Global.asax file :
protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs e)
{
if (FormsAuthentication.CookiesSupported == true)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
//let us take out the username now
string username = FormsAuthentication.Decrypt(Request.Cookies[FormsAuthentication.FormsCookieName].Value).Name;
string roles = string.Empty;
using (TrainingContext entities = new TrainingContext())
{
User user = entities.Users.SingleOrDefault(u => u.UserName == username);
roles = "admin";//user.Roles;
}
//Let us set the Pricipal with our user specific details
e.User = new System.Security.Principal.GenericPrincipal(
new System.Security.Principal.GenericIdentity(username, "Forms"), roles.Split(';'));
}
catch (Exception)
{
//somehting went wrong
}
}
}
}
When I log in my FormsAuthentication_OnAuthenticate executes and everything looks good. My User is set and my roles in the session is also there...
But when I click on the details of my Employee/Index screen it takes me back to the login screen (I expect it to take me to the details of the employee I clicked because I am logged in and I am setup as an admin role)
Please can you assist me to try and get to the problem. I sat for more than 18 hours already trying to figure this out.
I already looked at these solutions and as you can see most of my code comes from there...
codeproject.com/Articles/578374/AplusBeginner-27splusTutorialplusonplusCustomplusF
codeproject.com/Articles/342061/Understanding-ASP-NET-Roles-and-Membership-A-Begin
codeproject.com/Articles/408306/Understanding-and-Implementing-ASP-NET-Custom-Form
in case you need more detail about my code you can also download it from GitHub
https://github.com/Ruandv/Training/tree/FormsAuthentication
I will appreciate your assistance.

If you go to your Database and look for the table that assigns roles to users (probably generated by SimpleMembership?), does your user have an "admin" role?
Looks like you're only assigning the role in the FormsAuthentication_OnAuthenticate method without actually setting it in the DB.
// Your method (...)
User user = entities.Users.SingleOrDefault(u => u.UserName == username);
roles = "admin";//user.Roles;
And, although I'm not entirely sure, [Authorize(Roles = "admin")] may be using your Role Provider and checking if the user has/doesn't have the role in the database.

Related

How to forbid a locked out user to login in .NET Core 6 Identity

I have a custom user store in my .NET Core 6 site. The corresponding user table in DB contains a field called Lockout.
When a user signs in, I need that field to be used to forbid the user to login if that field is true.
The Login action of the Account Controller has this code:
var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
var appUser = await _userManager.FindByNameAsync(model.Login);
if (appUser.Roles == null || appUser.Roles.Count == 0)
{
await PerformLogOff();
return Json("ERROR: No tiene autorización para ingresar al sistema.");
}
else
{
returnUrl ??= Request.Path.ToString();
appUser.LastLoggedOn = DateTime.Now;
if (!appUser.FirstLoggedOn.HasValue)
appUser.FirstLoggedOn = appUser.LastLoggedOn;
await _userManager.UpdateAsync(appUser);
return Json(returnUrl);
}
}
else if (result.IsLockedOut)
return Json("ERROR: Su usuario está bloqueado.");
else if (result.RequiresTwoFactor)
return Json("ERROR: El usuario requiere autenticación de doble factor.");
else
return Json("ERROR: Nombre de usuario o contraseña incorrectos.");
When a user is locked out, I need PasswordSignInAsync to return result.IsLockedOut.
How can I do it? Should I create a custom SignInManager? Notice that this has nothing to do with locking a user out when he fails to enter the password several times in login screen.
Thanks
Jaime
You can do it your self by adding another filed (e.g. IsActive) to the your ApplicationUser class. Then you can check it before the line "_signInManager.PasswordSignInAsync" with some code like this:
var user = await _userManager.Users.Where(s => s.UserName == model.Username).FirstOrDefaultAsync();
if (user != null)
{
if (!user.IsActive)
{
ModelState.AddModelError("", "User has been deactivated");
//Log the failure
return View();
}
}
I have read source code about SignInManager.cs. And it should support forbid lockout user.
For more details, check the blog below, it contains the code useful to you.
User Lockout with ASP.NET Core Identity
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IactionResult> Login(UserLoginModel userModel, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(userModel);
}
var result = await _signInManager.PasswordSignInAsync(userModel.Email, userModel.Password, userModel.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
var forgotPassLink = Url.Action(nameof(ForgotPassword),"Account", new { }, Request.Scheme);
var content = string.Format("Your account is locked out, to reset your password, please click this link: {0}", forgotPassLink);
var message = new Message(new string[] { userModel.Email }, "Locked out account information", content, null);
await _emailSender.SendEmailAsync(message);
ModelState.AddModelError("", "The account is locked out");
return View();
}
else
{
ModelState.AddModelError("", "Invalid Login Attempt");
return View();
}
}
If you really want result.IsLockedOut to be true then I can see two options:
You would need to extend UserManager or AspNetUserManager and override virtual method IsLockedOutAsync (code) so it will be returning false based on your Lockout property.
I don't know internals of your store but you could also manually set original LockoutEnd field in database to date from the next century e.g.:3000-01-01 when your application logic wants to set it to true or clear the field to unlock the user
For both options you need to watch out - SignInManager will only attempt to check lockouts if feature is enabled (code).
To enable it you should make sure that your custom UserStore implements IUserLockoutStore<TUser> interface. Simple override of SupportsUserLockout (code) virtual property to return always true in your custom UserManager might not work - because there are few other operations made on UserStore to count failures etc.
Probably you are not looking for opinion but I feel that your idea from comment to load user in Login action and check your custom property is the cleanest. It is not only about hacking your solution here - but it is usualy very harmful in development teams to change well documented, battle-tested frameworks like Identity to behave in a different way.

Using Roles with Forms Authentication

I'm using forms authentication in my MVC application. This is working fine. But not I want to adjust authorization to only allow people in certain roles. The logins correspond to users in active directory and the roles correspond to the groups the users are in.
For authentication, I simply call FormsAuthentication.SetAuthCookie(username, true) after verifying the login.
For authorizing, I first applied the attribute to the controllers I want to secure
[Authorize(Roles = "AllowedUsers")]
public class MyController
...
Next, I'm handling the OnAuthenticate event in global.asax.
protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs args)
{
if (FormsAuthentication.CookiesSupported)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(
Request.Cookies[FormsAuthentication.FormsCookieName].Value);
// Create WindowsPrincipal from username. This adds active directory
// group memberships as roles to the user.
args.User = new WindowsPrincipal(new WindowsIdentity(ticket.Name));
FormsAuthentication.SetAuthCookie(ticket.Name, true);
}
catch (Exception e)
{
// Decrypt method failed.
}
}
}
else
{
throw new HttpException("Cookieless Forms Authentication is not " + "supported for this application.");
}
}
With this when someone accesses the website they get the login screen. From there they can actually log in. However, somehow it doesn't save the auth cookie and they get a login screen after the next link they click. I tried adding a call to SetAuthCookie() in OnAuthenticate() but they made no difference.
Before I added this event handler to handle authorization, authentication worked fine. So somewhere in the framework User is being set. I'm wondering if this the correct approach and I'm just missing something or if I need a different approach.
What do I need to do to get this to work?
Thanks,
Scott
It seems like my initial approach won't work. I was trying to get ASP.NET to automatically load user roles from their AD account. No comment was given on whether this was possible. However, the research I've done indicates I'll have to write code to load AD group memberships into user roles.
The solution to creating the user principal that ASP.NET MVC uses appears to be to create it in FormsAuthentication_OnAuthenticate() and assign it to Context.User. It appears if I don't set Context.User ASP.NET MVC creates a user principal based off the auth ticket after FormsAuthentication_OnAuthenticate() returns. Additionally, ASP.NET MVC appears to do nothing with Context.User if I set it in FormsAuthentication_OnAuthenticate().
The following is what I ended up doing.
This is the code that handles authentication
public ActionResult LogOn(FormCollection collection, string returnUrl)
{
// Code that authenticates user against active directory
if (authenticated)
{
var authTicket = new FormsAuthenticationTicket(username, true, 20);
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
authCookie.Expires = DateTime.Now.AddMinutes(30);
Response.Cookies.Add(authCookie);
if (Url.IsLocalUrl(returnUrl)
&& returnUrl.Length > 1
&& returnUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase)
&& !returnUrl.StartsWith("//", StringComparison.OrdinalIgnoreCase)
&& !returnUrl.StartsWith("/\\", StringComparison.OrdinalIgnoreCase))
{
return Redirect(returnUrl);
}
else
{
return Redirect("~/");
}
}
return View();
}
I initially tried just calling FormsAuthentication.SetAuthCookie(username, true) instead of manually creating, encrypting, and adding it to the Response cookie collections. That worked in the development environment. However, it didn't after I published to the website.
This is the log off code
public ActionResult LogOff()
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
authCookie.Expires = DateTime.Today.AddDays(-1);
}
Response.Cookies.Add(authCookie);
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
FormsAuthentication.SignOut() doesn't seem to do anything after I switched to manually creating, encrypting, and adding the auth ticket to the response cookie collection in the logon code. So I had to manually expire the cookie.
This is the code I have for FormsAuthentication_OnAuthenticate()
protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs args)
{
HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null || string.IsNullOrWhiteSpace(authCookie.Value))
return;
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
UserData userData = null;
if (Application["UserData_" + authTicket.Name] == null)
{
userData = new UserData(authTicket.Name);
Application["UserData_" + authTicket.Name] = userData;
}
else
{
userData = (UserData)Application["UserData_" + authTicket.Name];
}
Context.User = new GenericPrincipal(new GenericIdentity(authTicket.Name), userData.Roles);
}
UserData is a class I created to handle caching user roles. This was needed because of the time it takes for active directory to return the group memberships the user belongs to. For completeness, the following is the code I have for UserData.
public class UserData
{
private int _TimeoutInMinutes;
private string[] _Roles = null;
public string UserName { get; private set; }
public DateTime Expires { get; private set; }
public bool Expired { get { return Expires < DateTime.Now; } }
public string[] Roles
{
get
{
if (Expired || _Roles == null)
{
_Roles = GetADContainingGroups(UserName).ToArray();
Expires = DateTime.Now.AddMinutes(_TimeoutInMinutes);
}
return _Roles;
}
}
public UserData(string userName, int timeoutInMinutes = 20)
{
UserName = userName;
_TimeoutInMinutes = timeoutInMinutes;
}
}
Roles can also be stored in a cookie and you have at least two options:
a role provider cookie (another cookie that supports the forms cookie), set with cacheRolesInCookie="true" on a role provider config in web.config. Roles are read the first time authorization module asks for roles and the cookie is issued then
a custom role provider that stores roles in the userdata section of the forms cookie, roles have to be added to the user data section of the forms cookie manually
The Authorization module asks the current principal for user roles, which, if role provider is enabled, either scans the role cookie (the first option) or fires the custom role provider methods.
Yet another, recommended approach is to switch to the Session Authentication Module (SAM) that can replace forms authentication. There are important pros, including the fact that SAM recreates ClaimsPrincipal out of the cookie and roles are just Role claims:
// create cookie
SessionAuthenticationModule sam =
(SessionAuthenticationModule)
this.Context.ApplicationInstance.Modules["SessionAuthenticationModule"];
ClaimsPrincipal principal =
new ClaimsPrincipal( new GenericPrincipal( new GenericIdentity( "username" ), null ) );
// create any userdata you want. by creating custom types of claims you can have
// an arbitrary number of your own types of custom data
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "role1" ) );
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "role2" ) );
var token =
sam.CreateSessionSecurityToken(
principal, null, DateTime.Now, DateTime.Now.AddMinutes( 20 ), false );
sam.WriteSessionTokenToCookie( token );
From now on, the identity is stored in a cookie and managed automatically and, yes, the Authorization attribute on your controllers works as expected.
Read more on replacing forms module with SAM on my blog:
http://www.wiktorzychla.com/2012/09/forms-authentication-revisited.html

How to correct issue with user being redirected to login after successful, but not to user page?

In MVC4, I created a custom membership provider that returns true if the user authentication passes. No biggie here - this portion works the way it should:
public override bool ValidateUser(string username, string password)
{
var crypto = new SimpleCrypto.PBKDF2(); // type of encryption
// TODO: using (var unitOfWork = new Website.Repository.UnitOfWork(_dbContext))
//var unitOfWork1 = new Website.Repository.UnitOfWork(_dbContext);
using (var db = new Website.DAL.WebsiteDbContext())
{
var user = db.Users
.Include("MembershipType")
.FirstOrDefault(u => u.UserName == username);
if (user != null && user.Password == crypto.Compute(password, user.PasswordSalt))
{
FormsAuthentication.SetAuthCookie(username, true);
return true;
}
}
return false;
}
In my Login Action:
[HttpPost]
[AllowAnonymous]
public ActionResult Login(Models.UserModel user)
{
if (ModelState.IsValid)
{
// custom membership provider
if (Membership.ValidateUser(user.UserName, user.Password))
{
// Cannot use this block as user needs to login twice
//if (User.IsInRole("WaitConfirmation")) // checks the custom role provider and caches based on web.config settings
//{
// //TempData["EmailAddress"] = thisUser.Email;
// // email address has not yet been confirmed
// return RedirectToAction("WaitConfirmation");
// //return View("Account", thisUser)
//}
//else
//{
// // get custom identity - user properties
// string userName = UserContext.Identity.Name;
// //CustomIdentity identity = (CustomIdentity)User.Identity;
// var identity = UserContext.Identity;
// int userId = identity.UserId;
// return RedirectToAction("Index", "Dashboard");
//}
if (User.Identity.IsAuthenticated && User.IsInRole("WaitConfirmation")) // checks the custom role provider and caches based on web.config settings
{
return RedirectToAction("WaitConfirmation");
}
else if (User.Identity.IsAuthenticated)
{
// get custom identity - user properties
string userName = UserContext.Identity.Name;
return RedirectToAction("Index", "Dashboard");
}
}
else
{
ModelState.AddModelError("", "Login data is incorrect.");
}
}
return View(user);
}
In stepping through the code, when a user first logs in, User.Identity.IsAuthenticated is false and the page is redirected back to the login page. At this point if I either:
manually navigate to the user page (Dashboard) the user is details are available
login again, this works
I believe the answer lies somewhere in why the User.Identity.IsAuthenticated is not immediately true but can't figure out this is false the first time around.
The first block of commented-out code fails with Unable to cast object of type 'System.Security.Principal.GenericIdentity' to type 'Website.AdminWebsite.Infrastructure.CustomIdentity' as there is no IsAuthenticated check.
Suggestions?
This post describes a problem with similar symptoms.
http://forums.asp.net/t/1177741.aspx
Please have a read and ensure the order of your events (i.e. Authenticate, LoggedIn)
After reading the article #mcsilvio's suggested, I added an RedirectToAction() as follows to initiate a new page life-cycle:
public ActionResult Login(Models.UserModel user)
{
if (ModelState.IsValid)
{
// custom membership provider
if (Membership.ValidateUser(user.UserName, user.Password))
{
return RedirectToAction("VerifyIdentity", user);
}
else
{
ModelState.AddModelError("", "Login data is incorrect.");
}
}
return View(user);
}
public ActionResult VerifyIdentity(Models.UserModel user)
{
if (User.Identity.IsAuthenticated && User.IsInRole("WaitConfirmation")) // checks the custom role provider and caches based on web.config settings
{
return RedirectToAction("WaitConfirmation");
}
else if (User.Identity.IsAuthenticated)
{
// get custom identity - user properties
string userName = UserContext.Identity.Name;
return RedirectToAction("Index", "Dashboard");
}
return View(User);
}
This did the trick, but I'm wondering if there is a better way or is it always done like this?

Custom Role Provider variables

I have created a custom Role provider with the following overridden method:
public override string[] GetRolesForUser(string username)
{
MyAppUser user = userRepository.Get(u => u.Username == username).FirstOrDefault();
//MyAppUser user = userRepository.Get(u => u.Username == "testuser").FirstOrDefault();
if (user == null)
{
string[] roles = new string[1];
roles[0] = "Fail";
return roles;
}
else
{
Role role = roleRepository.Get(r => r.RoleID == user.RoleID).FirstOrDefault();
if (role == null)
{
string[] roles = new string[1];
roles[0] = "Fail";
return roles;
}
else
{
string[] roles = new string[1];
roles[0] = role.Name;
return roles;
}
}
}
Upon clicking on a section of the site that is authorized only to Admin I am successfully hitting the above method when passing in the username directly "testuser" but otherwise my username parameter is always blank. Where is this parameter populated? And how can I have it so that my current signed in user is checked here, I have a class called MyAppUser that holds user details but authentication is done outside of the app by ADFS and so we have no authentication inside of the project.
Where is this parameter populated?
Normally GetRolesForUser method is called from client code (code that you write in your application), so onus is on you to pass username. UserName is normally the login user name that user has used to login into your system so you should have it.
In case you want to discover it programmatically, you might be able to use
System.Web.HttpContext.Current.User.Identity.Name
in ASP.NET MVC.

How to extend/customize MVC4 Internet Application WebSecurity/SimpleMembership

I've been trying my best to search for more information on how to modify/extend/customize the default membership system available in MVC4 Internet Application (EF 5 Code First) in Visual Studio 2012 Express.
I would like to know how to implement email verification such that when a user registers an email is sent with an activation link. When they click on the link their account is activated and they can log in using their username or email.
I would also like to know how to implement simple Roles for registered users by assigning a default role during registration.
Similar Questions:
How do I manage profiles using SimpleMembership?
How do you extend the SimpleMembership authentication in ASP.NET MVC4
But I would really like to work with the existing simplemembership system.
This post is quite close:
http://blog.longle.net/2012/09/25/seeding-users-and-roles-with-mvc4-simplemembershipprovider-simpleroleprovider-ef5-codefirst-and-custom-user-properties/
I've also seen this post:
http://weblogs.asp.net/jgalloway/archive/2012/08/29/simplemembership-membership-providers-universal-providers-and-the-new-asp-net-4-5-web-forms-and-asp-net-mvc-4-templates.aspx
This is the closest I've found so far:
http://weblogs.asp.net/thangchung/archive/2012/11/15/customize-the-simplemembership-in-asp-net-mvc-4-0.aspx
This is also useful but for WebPages:
http://blog.osbornm.com/archive/2010/07/21/using-simplemembership-with-asp.net-webpages.aspx
I was hoping to find a more comprehensive walk-through on the proper way to extend it.
It does not look like you got any answer.
Unless I do not fully understand what you want to do, there is no need to modify/extend/customize the default SimpleMembership to provide an email registration mechanism, or assign a default role during registration, as all of this can be done inside AccountController.
As an example, here is a register method I am using:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid) //TODO Change this to use a worker to send emails.
{
// Check if email exists already before creating new user
using (UsersContext db = new UsersContext())
{
UserProfile email = db.UserProfiles.FirstOrDefault(u => u.Email.ToLower() == model.Email.ToLower());
UserProfile uName =
db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == model.UserName.ToLower());
// Attempt to register the user
try
{
if (email == null && uName == null && this.IsCaptchaVerify("Captcha is not valid"))
{
bool requireEmailConfirmation = !WebMail.SmtpServer.IsEmpty();
string confirmationToken = WebSecurity.CreateUserAndAccount(model.UserName, model.Password, new
{
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Email
},
requireEmailConfirmation);
if (requireEmailConfirmation)
{
EmailViewModel eml = new EmailViewModel
{
ToEmail = model.Email,
Subject = "Confirmez votre inscription",
FirstName = model.FirstName,
LastName = model.LastName,
Body = confirmationToken
};
UserMailer.ConfirmRegistration(eml).SendAsync();
Response.Redirect("~/Account/Thanks");
}
else
{
WebSecurity.Login(model.UserName, model.Password);
Response.Redirect("~/");
}
}
else
{
if (email != null)
ModelState.AddModelError("Email", "Email address already exists. Please enter a different email address.");
if (uName != null)
ModelState.AddModelError("UserName", "User Name already exists. Please enter a different user name.");
if (!this.IsCaptchaVerify("Captcha is not valid"))
TempData["ErrorMessage"] = "Captcha is not valid";
}
}
catch (MembershipCreateUserException e)
{
ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
}
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
There is no default role assigned here, but it would be easy to add once EmailConfirmation is validated.
As the question is quite old, I hope it help someone!