In MVC4 i used Roles.IsUserInRole to check if a given user is in some role. However, with MVC5 i can't do it anymore...
At first, it asked me to enable RoleManager at the web.config but then i discovered that microsoft moved away from Web.Security to Microsoft.AspNet.Identity.
My question now is, with Microsoft.AspNet.Identity how do i do an action similar to Roles.IsUserInRole?
And/or create a relation between the Role and the User.
By the way, i'm still trying to understand the new authentication methods (ClaimsIdentity?).
You should read http://typecastexception.com/post/2014/04/20/ASPNET-MVC-and-Identity-20-Understanding-the-Basics.aspx for the basics of Identity 2.0!
There's also a complete demo project to get you started:
https://github.com/TypecastException/AspNet-Identity-2-With-Integer-Keys
If you take this as the basis for your Identity foundation you'll end up with something like this:
var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
const string name = "YourUsername"
const string roleName = "Admin";
var user = userManager.FindByName(name);
//check for user roles
var rolesForUser = userManager.GetRoles(user.Id);
//if user is not in role, add him to it
if (!rolesForUser.Contains(role.Name))
{
userManager.AddToRole(user.Id, role.Name);
}
The post above was really helpful (Thanks Serv, would vote up if my reputation allowed me to). It helped me solve an issue I was having with a few minor changes to fit what I was trying to achieve. My particular problem was that I wanted to check in an MVC view if the current user was in a given role group. I also found that Roles.IsUserInRole was no longer working.
If you are doing this in a view, but using ASP.NET identity 2.0 instead of the simple membership provider offered by previous MVC versions, the following might be helpful as a 1-line solution:
bool isAdmin = HttpContext.Current.User.IsInRole("Admin");
You may then combine it with HTML to selectively show menu items (which is what I've been using it for) with something like this:
#if (isAdmin)
{
<li>#Html.ActionLink("Users", "List", "Account")</li>
}
This allows me to prevent access to the user management hyperlinks where the user is not a member of the 'Admin' role.
Related
Using asp.net Core, Mvc and OpenIdConnect, is it possible to prompt an authenticated user for additional information during the ODIC authentication process, and then redirect back to the originally-desired page?
To give a concrete example: in our system one person, represented by an email address, can have multiple user ids that they may wish to operate under. Assume my email address is tregan#domain.com, and I have 3 user ids to choose from: treganCat, treganDog, treganMouse. When I hit a Controller action that is decorated with the [Authorize] attribute I first go through OpenIdConnect authentication, and one of the claims returned is an email address.
Using that email address, I want the application to prompt me to select the identity that I want to run under (treganDog, treganCat, or treganMouse).
From there, I want the application to take the user id that I selected, interrogate a database for the roles that go along with the selected user id, and load those roles as claims to my identity.
Finally, I want the application to send me on to my desired page (which is the protected Controller method that I originally attempted to visit).
Is this possible?
I'm using an Owin Startup class; the code below "works" except for the fictional line "var identityGuid = [return value from the prompt];" ("fictional" because it represents what I would like to occur, but in fact a series of redirects would be needed).
My example below uses the OnTicketReceived event, but that selection is arbitrary, I would be willing to do this in any event.
services.AddAuthentication(authenticationOptions =>
{
authenticationOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
authenticationOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(openIdConnectOptions =>
{
openIdConnectOptions.Authority = Configuration["PingOne:Authority"];
openIdConnectOptions.CallbackPath = "/Callback";
openIdConnectOptions.ClientId = Configuration["PingOne:ClientId"];
openIdConnectOptions.ClientSecret = Configuration["PingOne:ClientSecret"];
openIdConnectOptions.ResponseType = "code";
openIdConnectOptions.Events.OnTicketReceived = (ticketReceivedContext) =>
{
var emailClaim =
ticketReceivedContext.Principal.Claims.FirstOrDefault(o =>
o.Type == ClaimTypes.Email);
string emailAddress = emailClaim.Value;
//here is where I would like to prompt the user to select an identity based on the email address
//the selected identity is represented by a guid
var identityGuid = [return value from the prompt];
var roles = new MyRepository(myContext).GetRolesForUserId(identityGuid);
var claims = new List<Claim>();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
ticketReceivedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
return Task.CompletedTask;
};
});
This is impersonation where there is a real user and you need to identify the impersonated user after login.
You will need to complete the login first, return to the app and configure the principal. Then render a UI and receive the selected choice.
You then need your UI to call the back end and tell it to update claims in the auth cookie. Not sure if you'll get this to work though - the impersonated user may need separate storage - such as a second cookie.
This highlights that it can be useful to separate the token / credential the UI receives from the claims the back end works with.
I use the below design a lot for REST APIs that serve UIs directly - though it may be overkill for your solution:
https://authguidance.com/2017/10/03/api-tokens-claims/
I think what I want to do is simply not possible without either figuring out a way to do it inside PingOne or writing my own IdentityServer and taking care of the extra steps there.
I decided to instead write a custom middleware that fires after the Authentication middleware, as described in this SO question: In asp.net core, why is await context.ChallengeAsync() not working as expected?
Currently developing a couple of applications (MVC and Web API) where my MVC application will send the request to the API to get authenticated and "login". I've got it working so that all my MVC application has to do is store the bearer token from the Web API and attach it anytime it needs to make a request for data.
At this point in the program we are looking to start working with Authorization and some security trimming to limit which users can make certain requests to the API and which users are able to see certain pages on the MVC application. In order to do this on both ends I need to get the Roles from my API and impersonate the Identity user on the MVC side since they are technically already logged in. My problem right now is probably kind of simple, but I can't figure out how to declare the Roles when I generate an identity user. Right now I just need a test case that we can explicitly declare and then I can grab it later once we start passing Roles from the API.
Any idea how to make a functioning example out of this with Roles?
private IdentityUser GenerateIdentityUser(string IdNum, string userN)
{
var newUser = new IdentityUser
{
Id = IdNum,
UserName = userN,
// Roles =
SecurityStamp = DateTime.Now.Ticks.ToString()
};
return newUser;
}
The Roles property in the IdentityUser has a private setter (see codeplex source code). The constructor for IdentityUser always creates an empty list of roles, so that it won't be null.
To set a role you'll need to add the following line after initializing your newUser object:
newUser.Roles.Add(new IdentityUserRole {UserId = newUser.Id, RoleId = "your role id"});
Please help me decide how to do the authorization for my project.
The project is written in ASP.Net MVC and is an intranet application that lets users log in via Active Directory.
Users can either be Junior Lecturer, Senior Lecturer, HOD or SuperUser. That's easy enough with roles but the situation is more complicated than that because those roles are based on whatever information you are looking at. For instance, a Junior Lecturer can only see information for students that he lectures etc.
As far as I understand it, that's where claims come in. I should assign modules or students to a user.
But the process if further complicated because there is no list of AD usernames and their students/modules in any single place.
I need to be able to check several databases to see if a user should be allowed to see the information.
Sometimes though, a user who has access to see certain information will not pass any of those checks. Because of that I will have to create a database with an AD username, their role and their subjects/students.
So my question is really regarding authorization. Is it possible for me to have that level of control on the claims and how would I assign them and check them?
If its not possible - how am i going to do this authorization?
Having an if statement at the start of every Action in every Controller feels wrong - but that's all my colleagues can come up with.
If you create a class that inherits from AuthorizeAttribute then you have the freedom to do whatever complicated authorization process you see fit. See my answer here on how to do so.
Here is some code I am using to query and cache AD roles:
private static Dictionary<Tuple<string, string>, bool> groupIdentityCache = new Dictionary<Tuple<string, string>, bool>();
..
public static bool UserHasRole(IIdentity identity, string groupShortName)
{
// (we rename our actual AD roles to shorter ones relevant to the site
// e.g. [MyAuthorizeAttribute(Roles = "Support,Admin")])
if (!AdLongGroupNames.ContainsKey(groupShortName.ToUpper())) return false;
string fullADGroupName = AdLongGroupNames[groupShortName.ToUpper()];
Tuple<string, string> key = new Tuple<string, string>(identity.Name.ToUpper(), groupShortName.ToUpper());
if (!groupIdentityCache.ContainsKey(key))
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, "DOMAINNAME"))
{
using (GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, fullADGroupName))
{
using (UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(principalContext, GetLogin(identity)))
{
groupIdentityCache[key] = userPrincipal.IsMemberOf(groupPrincipal);
}
}
}
}
return groupIdentityCache[key];
}
public static string GetLogin(IIdentity identity)
{
string[] parts = identity.Name.Split('\\');
if (parts.Count() < 2) return parts[0]; else return parts[1];
}
If you cache role memberships, you must also clear the cache on Session_Start for changes to take effect.
You can modify this solution to include roles from non-AD sources (such as a databases of class membership etc.) by adding to the groupIdentityCache dictionary. Modifying groupIdentityCache can also help when testing.
Having an 'if' statement is wrong, it will cost you long iteration and a huge misuse of memory.
Basically, you can control your authorization at every level almost using the authorize attribute.
However, you do need a well-built entity to do so, in order to not get lost withing all of the rules. It is very recommended that you will have a proper plan of your authorization scheme before committing this.
You can try to use switch statements instead, which will be a lot better in practice.
Please note:
Authorization Article that might help you understand.
I have an ASP.Net MVC application using Windows Authentication, and I am checking group membership for security on controller actions.
Simple as it sounds, I've found no other Question that can resolve the problem I am experiencing.
First Attempt: [Authorize]
The classic method is to simply slap an Authorize data annotation attribute on the controller action and go to town:
[Authorize(Roles = #"domain\groupName1")]
No dice. I am prompted for credentials. Usually this means something is wrong with the Windows Authentication configuration but it's setup fine: (1) HttpContext.User is a WindowsPrincipal object, and (2) I confirmed another known group name works.
Second Attempt: IsInRole()
The next step taken was to go a more old fashioned route and use IPrincipal.IsInRole(), and again, one returns false, the other true.
var wp = (WindowsPrincipal)User;
// false
var inGroup1 = wp.IsInRole(#"domain\groupName1");
// true
var inGroup2 = wp.IsInRole(#"domain\groupName2");
Stumped... so I hit up my systems nerds and we double check everything. User is a group member? Yes. Group name is spelled correctly? Yes. The next step was to snag the SID.
Third Attempt: Search Identity's Group Collection
In my controller I check the WindowsIdentity and look through the group collection for the SID of the troublesome group:
var wi = (WindowsIdentity)wp.Identity;
var group = wi.Groups.SingleOrDefault(g => g.Value == "group1-sidValue");
The group variable is the SecurityIdentifier object. Because it is not null, we can be certain that this current user is a member of the group that both the [Authorize()] or IsInRole() attempts fail to confirm.
Fourth Attempt: DirectoryServices.AccountManagement
At this point, I'm going nuts and add reference to the AccountManagement APIs. I search the domain context for the GroupPrincipal by both name and SID:
var pc = new PrincipalContext(ContextType.Domain, "domain");
var gp1byName = GroupPrincipal.FindByIdentity(pc, "groupName1")
var gp1bySid = GroupPrincipal.FindByIdentity(pc, IdentityType.Sid, "group1-sidValue");
Both group principal variables are ripe with the same object, and I verified through a watch variable that the principal's Members collection contains a UserPrincipal object with the same SID as the current WindowsPrincipal on HttpContext.
Question:
What in the hell have I missed here? Why would both role checking methodologies fail when it is plain and clear through object exploration that the user is a valid member of this given group?
The fact that one group checks fine and the other does not seems the most strange part at this point.
Answer:
Essentially it's translation issues between WindowsIdentity and NTAccount (both of these System.Security.Principal) and lastly, the actual Active Directory entry.
When validating a WindowsIdentity against AD, if you want to use anything other than the Sam or the Sid, you will need to use System.DirectoryServices.AccountManagement.
Caveat: In .Net 4.5 the security principals include Claims but that's out of context.
Long Explanation:
In a Windows Authenticated web application, HttpContext.User is a WindowsPrincipal object wrapping an underlying WindowsIdentity.
WindowsIdentity has for most intents and purposes only two properties with which the authenticated user can be identified: Name and User.
These properties translate to two properties on the identity's corresponding AD account entry:
WindowsIdentity.Name = SamAccountName
WindowsIdentity.User = SID
The [Authorize] filter attribute ultimately calls IsInRole(string role) on the underlying principal... and the IsInRole() string overload instantiates an NTAccount with the role (the "SamAccountName" in an AD entry).
This explains the failure in #1 and #2 above.
To authorize the HttpContext.User against anything but his/her Sid or SamAccountName, you'll need DirectoryServices.AccountManagement or classic LDAP.
I have an ASP.Net MVC application using Windows Authentication, and I am checking group membership for security on controller actions. Simple as it sounds, I've found no other Question that can resolve the problem I am experiencing.
Took me a lot of time to find something
http://www.c-sharpcorner.com/uploadfile/scottlysle/test-for-user-group-membership-in-Asp-Net-C-Sharp/
My code to check if user belongs to an AD group :
foreach (System.Security.Principal.IdentityReference group in System.Web.HttpContext.Current.Request.LogonUserIdentity.Groups)
{
if (String.Equals(group.Translate(typeof(System.Security.Principal.NTAccount)).ToString(), #"your_domain_name\your_group_name", StringComparison.InvariantCultureIgnoreCase))
{
// the user belongs to a group
}
}
That is all I needed beside <authentication mode="Windows"/> in Web.config file
I have basic Single Sign-On working across 2 MVC sites (call them SiteA and SiteB) using something along the lines of the following method:
http://forums.asp.net/p/1023838/2614630.aspx
They are on sub-domains of the same domain and share hash\encryption keys etc in web.config. I've modified the cookie so it is accessible to all Sites on the same domain. All of this seems to be working ok.
The sites are on separate servers without access to the same SQL database, so only SiteA actually holds the user login details. SiteB has a membership database, but with empty users.
This works fine for my required scenario which is:
1) User logs into SiteA
2) The application loads data from SiteA (by AJAX) and SiteB (by AJAX using JSONP)
I have the following LogOn Action on my AccountController for SiteA, which is where the "magic" happens:
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
//modify the Domain attribute of the cookie to the second level of domain
// Add roles
string[] roles = Roles.GetRolesForUser(model.UserName);
HttpCookie cookie = FormsAuthentication.GetAuthCookie(User.Identity.Name, false);
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
// Store roles inside the Forms cookie.
FormsAuthenticationTicket newticket = new FormsAuthenticationTicket(ticket.Version, model.UserName,
ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, String.Join("|", roles), ticket.CookiePath);
cookie.Value = FormsAuthentication.Encrypt(newticket);
cookie.HttpOnly = false;
cookie.Domain = ConfigurationManager.AppSettings["Level2DomainName"];
Response.Cookies.Remove(cookie.Name);
Response.AppendCookie(cookie);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
This does some stuff which I don't strictly need for the initial scenario, but relates to my question. It inserts the Roles list for the user on login to SiteA into the UserData of the authentication ticket. This is then "restored" on SiteB by the following in global.asax:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (Context.Request.IsAuthenticated)
{
FormsIdentity ident = (FormsIdentity) Context.User.Identity;
string[] arrRoles = ident.Ticket.UserData.Split(new[] {'|'});
Context.User = new System.Security.Principal.GenericPrincipal(ident, arrRoles);
}
}
All of the stuff above works until I add Roles into the mix. Things work fine if I only decorate my Controllers\Actions on SiteB with [Authorize] attributes. But as soon as I add [Authorize(roles="TestAdmin")] users can no longer access that Controller Action. Obviously I have added the user to the TestAdmin Role.
If I debug the global.asax code on SiteB, it looks ok as I leave the global.asax code, BUT then when I hit a break point in the controller itself the Controller.User and Controller.HttpContext.User is now a System.Web.Security.RolePrincipal without the roles set anymore.
So my question is: Does anybody have any idea how I can restore the roles on SiteB or another way to do this?
You already worked it out, but here we go:
make it work: turn off the role manager. Its not an odd behavior that asp.net is doing that, since you are explicitly telling it to use look for the user's roles with the configuration specified.
another way to do it: enable the role manager in both. Use the configuration to share the cookie as you are doing in your custom code. Based on your description, you shouldn't need to worry about it trying to get roles for the user, as long as you use a matching configuration for the authentication cookie
should you use Application_AuthorizeRequest to set the roles cookies? imho opinion earlier (Authenticate) is best, I have always done it that way and never ran into issues.
Since this seems to have stagnated I can partially answer this one with some additional findings. After debugging\testing this a bit more, it seems MVC2 is doing something odd after leaving Application_AuthenticateRequest but before entering my Controller. More details here:
http://forums.asp.net/t/1597075.aspx
A workaround is to use Application_AuthorizeRequest instead of Application_AuthenticateRequest.
EDIT: I believe I found the cause of my issues. On my MVC1 project I had roleManager disabled but my test MVC2 project had roleManager enabled. Once I enabled my roleManager in the MVC1 test project, the behaviour between MVC1 and MVC2 is the same. I also have a question to one of the MVC Microsoft team open about this, and will post back here if Application_AuthorizeRequest is the correct place to restore the Roles from Authentication ticket cookie...