Active Directory Group Membership Checking in .Net 4.5 - c#

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

Related

How can I uniquely identify Active Directory guest users in a Blazor/ASP application

I am using a Blazor application with Azure Active Directory authentication and guest users. When a user logs in, I want to use their authenticated identity as a key to lookup permissions and other attributes in my own database. Initially I used this code...
#using Microsoft.AspNetCore.Components.Authorization
#inject AuthenticationStateProvider AuthenticationStateProvider
#code {
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
//Don't do this: Name != email and may not even be constant
var email = user.Identity.Name;
var attributes = await myDatabase.FetchAttributesForUser(email);
//use attributes....
}
}
However, user.Identity.Name is not the email address of the user. As described in this old post, it is simply an identifier that the authentication provider supplies. For example, a user with an outlook address of first.last#outlook.com might be authenticated with a Name of live#first.last#outlook.com There's therefore no guarantee that the Name may be unique across providers or even across time for the same provider.
This stackoverflow question is identifying the same problem and the accepted answer is to use the SID but what is missing is an explanation of how I can retrieve the SID from the AuthenticationStateProvider that's been injected into my application. (There are no obvious fields or paths that lead to a SID.) Should I be injecting some other authentication object?
Also, if the recommendation is to use the SID, how can I obtain it for pre-provisioning? I.e. I invite "jo.bloggs#contoso.com" and want to set up attributes in my database before she first logs in. Is the "object id" shown in the portal actually the SID?
In Azure AD you can use either the oid or sub claim.
The oid claim contains the object id for the user in the Azure AD tenant they signed into.
It is thus unique across applications.
The sub claim is unique for that user in one application.
Both of them are immutable.
A .NET ClaimsPrincipal may contain the oid claim with a different name: http://schemas.microsoft.com/identity/claims/objectidentifier.
One source for these alternate names is the System.Security.Claims.ClaimTypes class.
Claims in id tokens: https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens#payload-claims
A general solution for obtaining the SID from the AuthenticationStateProvider with respect to the below in point asked in the original question.
This stackoverflow question is identifying the same problem and the accepted answer is to use the SID but what is missing is an explanation of how I can retrieve the SID from the AuthenticationStateProvider that's been injected into my application. (There are no obvious fields or paths that lead to a SID.) Should I be injecting some other authentication object?
If you notice, the AuthenticationState's User property returns the Identity property of type IIDentity which has the base functionalities for WindowsIdentity and other Identities as well. Hence converting the Identity into the WindowsIdentity becomes valid here. And after converting you can get the SID information of the user as below. The other information of the user can be obtained directly from the identity object.
var authenticationState = await authenticationStateTask;
WindowsIdentity identity = authenticationState.User.Identity as WindowsIdentity;
var SId = identity.User.Value;
Hope it helps.

MVC 5 - Roles - IsUserInRole and Adding user to role

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.

Using UserPrincipal.FindByIdentity and PrincipalContext with nested OU

Here is what I am trying to achieve:
I have a nested OU structure that is about 5 levels deep.
OU=Portal,OU=Dev,OU=Apps,OU=Grps,OU=Admin,DC=test,DC=com
I am trying to find out if the user has permissions/exists at OU=Portal.
Here's a snippet of what I currently have:
PrincipalContext domain = new PrincipalContext(
ContextType.Domain,
"test.com",
"OU=Portal,OU=Dev,OU=Apps,OU=Grps,OU=Admin,DC=test,DC=com");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, myusername);
PrincipalSearchResult<Principal> group = user.GetAuthorizationGroups();
For some unknown reason, the value user generated from the above code is always null. However, if I were to drop all the OU as follows:
PrincipalContext domain = new PrincipalContext(
ContextType.Domain,
"test.com",
"DC=test,DC=com");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, myusername);
PrincipalSearchResult<Principal> group = user.GetAuthorizationGroups();
this would work just fine and return me the correct user. I am simply trying to reduce the number of results as opposed to getting everything from AD.
Is there anything that I am doing wrong? I've Googled for hours and tested various combinations without much luck.
Well, if
UserPrincipal.FindByIdentity(context, identityType, username) == null
then the user has not been found, which in your case probably is, because the user isn't defined in the OU= you are setting as container in your Context.
After much exploring, experimentation, googling and searching through stack overflow; it appears that .NET does not have a built in method to 'read' a particular OU that has a reference to an external Group that contains users as its members. Unfortunately, the suggested and recommended solution is to retrieve at domain level and perform some form of custom filtering.
Is the user you're looking for inside OU=Portal,OU=Dev,OU=Apps,OU=Grps,OU=Admin,DC=test,DC=com ?
What does your user object look like after your second search? What is it's DistinguishedName property?
The search you have in your first example will only search for objects inside that sub-sub-sub-sub-OU (the OU=Portal, .... that you have).
If your user exists in some other OU, then you have to search from the top of the domain - or inside the OU where the user actually exists (or any of its parents).
The user does not exist there, or you would not get null returned.
What is your end game? What do you mean by:
I am trying to find out if the user has permissions at
OU=Portal.
What type of permissions are you looking for? Admin delegation?
Hope this is of some help, I was having the same problem trying to retrieve groups from a nested OU. The structure of the ou was Groups > WebGroups. So I was writing the following...
var ctx = new PrincipalContext(ContextType.Domain, "domain", "OU=Groups,OU=WebGroups,DC=domain,DC=ie", "username", "password")
Turns out the order matters, WebGroups has to come first. When I changed it to the following my code worked...
var ctx = new PrincipalContext(ContextType.Domain, "domain", "OU=WebGroups,OU=Groups,DC=domain,DC=ie", "username", "password")
So I'm assuming you'd have to write "OU=Admin,OU=Groups... OU=Portal" to get yours working.

ASP.NET Active Directory C# field specification

We've got an active directory here. provided the unique user id of the user, I need to access the organization->manager->name attribute related to that userid. Basically this will be used to send an approval form to the manager of the person submitting request.
Any idea how this could be done?
You can use the following code :
/* Retreiving object from SID
*/
string SidLDAPURLForm = "LDAP://WM2008R2ENT:389/<SID={0}>";
System.Security.Principal.SecurityIdentifier sidToFind = new System.Security.Principal.SecurityIdentifier("S-1-5-21-3115856885-816991240-3296679909-1106");
/*
System.Security.Principal.NTAccount user = new System.Security.Principal.NTAccount("SomeUsername");
System.Security.Principal.SecurityIdentifier sidToFind = user.Translate(System.Security.Principal.SecurityIdentifier)
*/
DirectoryEntry userEntry = new DirectoryEntry(string.Format(SidLDAPURLForm, sidToFind.Value));
string managerDn = userEntry.Properties["manager"].Value.ToString();
But you can also find in this post other ways to seach bind to Active-directory.
Since you're on .NET 3.5 and up, you should check out the System.DirectoryServices.AccountManagement (S.DS.AM) namespace. Read all about it here:
Managing Directory Security Principals in the .NET Framework 3.5
MSDN docs on System.DirectoryServices.AccountManagement
Basically, you can define a domain context and easily find users and/or groups in AD:
// set up domain context
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(ctx, "SomeUserName");
if(user != null)
{
// do something here....
}
// find the group in question
GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, "YourGroupNameHere");
// if found....
if (group != null)
{
// iterate over members
foreach (Principal p in group.GetMembers())
{
Console.WriteLine("{0}: {1}", p.StructuralObjectClass, p.DisplayName);
// do whatever you need to do to those members
}
}
The new S.DS.AM makes it really easy to play around with users and groups in AD!
I'm not 100% sure what you want to do in your concrete case... the UserPrincipal has an EmployeeId property - is that what you want to search for?
Use the System.DirectoryServices.DirectoryEntry class to read out the appropriate property of the user object. The constructor of DirectoryEntry requires that you have an LDAP path to the user. Getting the LDAP path can often be tricky though as IIS prefers handing over the SAM account name only. If you provide more details of what the user id you have looks like it is easier to point you in the right direction.
To do this the account which runs the ASP.NET application needs read access to the AD, which probably doesn't have by default. Changing the application pool to run under "NetworkService" is the easiest way if the web server belongs to the AD. The ASP.NET app will then use the MACHINE$ account of the server to access the AD.

Is there a way to enable referral chasing for UserPrincipal.FindByIdentity()?

I have a .NET 3.5 web application that uses the System.DirectoryServices.AccountManagement classes. When I search for some users I get a PrincipalOperationException: A referral was returned from the server. If I did this the old school way with my own LDAP code I could enable chasing of referrals. Do I need to rewrite my code?
My code looks like this:
using (var principalContext = new PrincipalContext(ContextType.Domain, null, adPath))
{
// Find the principal object for which you wish to enumerate group
// membership.
using (var userPrincipal = UserPrincipal.FindByIdentity(principalContext, identity))
{
if (userPrincipal != null)
{
Name = userPrincipal.DisplayName;
DistinguishedName = userPrincipal.DistinguishedName;
EmailAddress = userPrincipal.EmailAddress;
Sid = userPrincipal.Sid.Value;
}
}
}
My adPath can be one of 2 values. One of the values is a domain that was recently joined, and can be accessed using different tools. I believe this is a problem with how this .NET library makes the LDAP calls.
Here is a partial Answer, as it's too long for a comment.
According to this Microsoft documentation, as you even know, Referrals are a hint that the client can chase. But concerning RODC they add "For example, in the case of an LDAP application, if chase referrals is enabled on the LDAP connection between the client and the RODC, the application never knows that the client received a referral from the RODC. The client is automatically redirected to the writable domain controller that is specified in the referral. ".
So I look how to enable LDAP chasing on a connexion in Microsoft site and I found this which means ADSI use. I'am very interested in the answer.
Do you try to query the global catalog like this :
/* Retreiving a principal context
*/
PrincipalContext domainContext = new PrincipalContext(ContextType.Domain, "YourGCServer:3268", "dc=dom,dc=fr", "User", "Password");
It's supposed to contains all the forest domain's datas.
I hope it helps.
Have you tried code of the form(put the domain in as the second argument):
var principalContext = new PrincipalContext(ContextType.Domain, "office.local", "OU=Users, DC=office, DC=local" ))
Also make sure that the adPath is from most specific to least specific.

Categories