Custom Role Provider variables - authentication

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.

Related

Asp.Net Core Mvc6 Claims Authorization is not persistent

I am doing an Asp.Net Core Mvc 6 App where I dynamically assign Claims to the user.
I have in the database all Roles that a user must have to access specific fields of the page.
I also have the role of the user.
I am creating Claims where the user's role matches the field page User role. And then I create a ClaimsPrincipal with those list of Roles.
This are the data in the database
This is how I create the permission for the user..
private string[] TransformRowsIntoPermissions(List<RolesAccessModel>? rows, string userRole)
{
List<string> permissionList = new();
if(rows!=null)
{
foreach (RolesAccessModel row in rows)
{
if (row.Roles!=string.Empty && row.Roles != null && !row.Roles.Contains(userRole))
continue;
// if we get here we have a match
if (!permissionList.Contains(row.EventName))
permissionList.Add(row.EventName);
}
}
return permissionList.ToArray();
}
When I have the Permission List, I add it as Claims
private Claim[] CreateClaimsFromArray(string[] permissions)
{
List<Claim> claims = new();
foreach (var permission in permissions)
claims.Add(new Claim(permission, "-"));
return claims.ToArray();
}
And in the main function I save it as ClaimPrincipal
private async void CreateClaimsByUserRole(string role)
{
ClaimsIdentity claimsIdentity =await _iUIConfig.CreateClaimsByUserRole(role);
var userPrincipal = new ClaimsPrincipal(new[] { claimsIdentity });
_ = HttpContext.SignInAsync(userPrincipal);
}
I do not know if line _ = HttpContext.SignInAsync(userPrincipal); is necessary.
I checked userPrincipal and it has all the data.
What I need to do is to ask in the View for that claims in order to show or to not Show the fields.
But when I ask if it is null...
#{
var claimsIdentity = User.Identity as System.Security.Claims.ClaimsIdentity;
if (claimsIdentity != null)
{
var c = claimsIdentity.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (c != null)
{
<p>
#c.Type.ToString();
#c.Value.ToString();
</p>
}
else
{
<p>
null
</p>
}
}
}
It is null
If I ask for a specific value
#{
if(System.Security.Claims.ClaimsPrincipal.Current.Claims.ToList().FirstOrDefault(c => c.Type == "DocumentId" && c.Value == "-") != null)
{
.....
}
}
Got an error System.NullReferenceException: 'Object reference not set to an instance of an object.'
Even If a call a Controller/Method and ask for
var identity = (ClaimsPrincipal)Thread.CurrentPrincipal;
Is null..
I have to do it persistent for all the App.
I am not using Authentication because it is not implemented yet.. I am using SSO from another system. For that reason, for my understanding Cookie are used for Authentication.
In program.cs I only have
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminAccess", policy => policy.RequireRole("Admin"));
options.AddPolicy("ManagerAccess", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin")
|| context.User.IsInRole("Manager")));
options.AddPolicy("UserAccess", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin")
|| context.User.IsInRole("Manager")
|| context.User.IsInRole("User")));
});
and
app.UseAuthorization();
What am I missing to do it persistent?
Thanks
ClaimsPrincipal.Current was used in asp.net,however,in Asp.net core it is no longer set
To retrieve the current user in an ASP.NET Core MVC app,you could try with ControllerBase.User orHttpContext.User
You could check this document for more details
SignInAsync creates an encrypted cookie and adds it to the current response.You could get the claims after authenticating successfully in the following request untill the cookie expires(not the current request)
Notice HttpContext.User could be setted directly
And in View you could get the claims in # section as below:
#{
var claims = this.ViewContext.HttpContext.User.Claims.ToList();
}

ASP.NET Core Identity Role, Claim and User

I am an ASP.NET Core beginner. I'm stuck in role, claim and user relationship.
I have a user Ben, user belongs to Admin role. Admin role has claims view-page and edit-page in database.
But I can't get claims and roles to be belonging to that user:
(Please see comment in code)
var user = await _userManager.FindByNameAsync(applicationUser.UserName);
if(user != null) {
var userClaims = await _userManager.GetClaimsAsync(user); // empty, WHY ?
var userRoles = await _userManager.GetRolesAsync(user); // ['admin']
var adminRole = DbContext.Roles.FirstOrDefault(x => x.Name == "Admin");
IList<Claim> adminClaims;
if(adminRole != null)
{
adminClaims = await _roleManager.GetClaimsAsync(adminRole);
// correct => ['view-page', 'edit-page']
}
}
}
In my mind, I understand when a user is a member of a role, he inherit that role's claims.
Default ASP.NET Identity have 5 tables:
Users.
Roles.
UserRoles - A user can have many roles.
RoleClaims - A role can have many claims.
UserClaims - A user can have many claims.
Do i think correct ? Why userManager.GetClaimsAsync(user) returns empty claims ?
Any suggestion?
Why userManager.GetClaimsAsync(user) returns empty claims ?
Because UserManager.GetClaimsAsync(user) queries the UserClaims table. Same for
RoleManager.GetClaimsAsync(role) queries the RoleClaims table.
But by design in ASP.NET Identity Core when a user is a member of a role, they automatically inherit the role's claims. You can check the ClaimsPrincipal, for example inside a controller action:
var claims = User.Claims.ToList();
You can see the code in UserClaimsPrincipalFactory.cs that creates a ClaimsPrincipal from an user.
I have had to deal with this issue recently and to solve the problem of locating Users by a particular Claim that came from a Role is to create a new Claim object with the values from the Role Claim:
var role = await roleManager.FindByNameAsync(yourRoleName);
if(role != null)
{
var roleClaims = await roleManager.GetClaimsAsync(role);
if(roleClaims != null && roleClaims.Count() > 0)
{
foreach(var claim in roleClaims.ToList())
{
var users = await userManager.GetUsersForClaimAsync(new Claim(claim.Type, claim.Value));
if(users != null && users.Count() > 0)
{
foreach(var user in users.ToList())
{
//This is an example of only removing a claim, but this is the
//area where you could remove/add the updated claim
await userManager.RemoveClaimAsync(user, new Claim(claim.Type, claim.Value));
}
}
}
}
}
This allowed me to Update/Delete a role with claims and pass those changes to the Users to be Re-Issued/Removed that were assigned the roles and claims. However, I am still looking for something more elegant/easier with less code.

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?

Form Authentication : Roles (MVC 4) C#

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.