Determined this can't be a network issue. I'm having this issue in debug (VS2012 / .Net 4.5 / IIS Express 8.0)
Code:
bool rtn2 = HttpContext.Current.User.IsInRole("MyDomain\\Domain Users");
Eventually returns true. But, can take several minutes.
var test = HttpContext.Current;
var test2 = HttpContext.Current.User;
var test3 = HttpContext.Current.User.Identity;
...all extremely fast.
var test = HttpContext.Current.User.IsInRole("MyDomain\\Domain Users");
var test2 = HttpContext.Current.User.IsInRole("MyDomain\\Domain Users");
First call takes several minutes, the second is instant. If I change the second to look for some other group (assuming the first was cached), it is still instant.
I thought maybe i'm having network issues (I connect to the domain and debug over VPN.) However, if I create a new VS2012 web project and put that code in the startup page, it's instant. I can also search Active Directory from my machine and pull up the Domain Users group and see all people in it pretty much instantly (there are over 10 thousand users) - no problem. So, this must be project / config based issue?
Going out of my mind trying to figure this out. Some info:
Tried re-installing IIS Express
I've tried rebooting
I've tried in a new tester web project - works instantly
Problem seems to be machine specific. Any assistance or even just recommendations for additional trouble-shooting steps would be appreciated.
Try using the System.DirectoryServices.AccountManagement namespace instead.
public static bool IsUserGroupMember(string userName, string groupName)
{
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
using (UserPrincipal user = UserPrincipal.FindByIdentity(context, userName))
using (PrincipalSearchResult<Principal> groups = user.GetAuthorizationGroups())
{
return groups.OfType<GroupPrincipal>().Any(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
}
}
I had the same problem. IsInRole was taking forever to finish on a production server. The following code worked instead. Saw it somewhere, unfortunately can't remember the source.
// Is in AD Group?
private static bool IsInADGroup(String inGroup)
{
foreach (System.Security.Principal.IdentityReference group in
System.Web.HttpContext.Current.Request.LogonUserIdentity.Groups)
{
String sGroup = (group.Translate(typeof(System.Security.Principal.NTAccount)).ToString());
if (sGroup.Equals(inGroup))
return true;
}
return false;
}
Related
I would like to use query by example to show me all the locked out accounts in my OU.
I was able to successfully do something similar with Enabled accounts and also Smart Card Logon Required accounts.
For some reason, userPrincipal.IsAccountLockedOut() seems to function different than userPrincipal.Enabled
Basically , it seems to be a method rather than a variable.
I searched online and couldn't find any relevant answers or documentation specific to this use case.
Here is my code, currently:
bool enabled = true;
bool locked = false;
string firstName = "John";
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
UserPrincipal up = new UserPrincipal(ctx);
up.Enabled = enabled;
up.GivenName = firstName;
But, up.IsAccountLockedOut() = locked; doesn't work. Neither does locked = up.IsAccountLockedOut()
IsAccountLockedOut() functions differently than Enabled since those are two different things:
An account is locked out by too many wrong password attempts. This is to prevent brute force attempts to guess a password. Accounts are usually automatically unlocked after a period of time. The number of wrong attempts that triggers a lockout and the time before automatic unlocking is configurable by the domain admin.
Disabling an account (Enabled == false) is when an administrator has specifically disabled the account. No one will be able to authenticate with a disabled account, even if they know the right password.
To find locked out accounts, you want to look at the lockoutTime attribute. It stores the time the account was locked out. A value of 0 means it's not locked. So you want to look for accounts where the value is greater than 0. This would be the LDAP query:
(&(objectCategory=person)(objectClass=user)(lockoutTime>=1))
You have to use >=1 since the LDAP spec doesn't actually support >.
I assume you're asking this question since you're trying to search with PrincipalSearcher, which limits you to searching based on properties that UserPrincipal exposes to you. Since the lockoutTime attribute is not exposed by UserPrincipal, you can't do it that way. You'll have to use DirectorySearcher directly (which is what PrincipalSearcher uses behind the scenes anyway). Here is an example that would output the username and the time that the lockout occurred:
var searcher = new DirectorySearcher() {
Filter = "(&(objectCategory=person)(objectClass=user)(lockoutTime>=1))",
PageSize = 1000, //make sure we get more than one page, if needed
PropertiesToLoad = { "sAMAccountName", "lockoutTime" } //which atrributes you want to use
};
using (var results = searcher.FindAll()) {
foreach (SearchResult result in results) {
var username = (string) result.Properties["sAMAccountName"][0];
var lockoutTime = DateTime.FromFileTime((long) result.Properties["lockoutTime"][0]);
Console.WriteLine($"{username} was locked out at {lockoutTime}");
}
}
Personally, I've stopped using UserPrincipal/PrincipalSearcher altogether because performance is always worse (sometimes it's not noticeable, other times it absolutely is), and there are times like this when you can't use it anyway. I wrote an article about getting the best performance when talking to AD, if you're interested: Active Directory: Better performance
Where I work, we have two modes of authentication:
CAS (http://www.jasig.org/cas)
LDAP
CAS is the primary method, but it is often unreliable at peak traffic times and so we have been using LDAP as a fallback mode for when we notice that CAS is down. Previously, we were using PHP for doing our LDAP fallback and got reasonable performance. There wasn't a noticeable delay during login other than the expected network lag times. A login took probably ~250-500ms to complete using LDAP.
Now, we are making a new system and have chosen ASP.NET MVC4 as the platform rather than PHP and I am tasked with trying to get this fallback working again. I have been pulling my hair out for about 6 hours now trying different things over and over again, getting the same result (perhaps I am insane). I have finally managed to connect to LDAP, authenticate the user, and get their attributes from LDAP. However, the query consistently takes 4.5 seconds to complete no matter what method I try.
This is very surprising to me seeing as the PHP version was able to do nearly the same thing in 1/8th the time and it would seem that the .NET framework has excellent support for LDAP/ActiveDirectory. Am I doing something incredibly horribly wrong?
Here are the guts of my function as it stands now (this one is the latest iteration that manages to do everything in one 4.5 second query):
public Models.CASAttributes Authenticate(string username, string pwd)
{
string uid = string.Format("uid={0},ou=People,o=byu.edu", username);
LdapDirectoryIdentifier identifier = new LdapDirectoryIdentifier("ldap.byu.edu", 636, false, false);
try
{
using (LdapConnection connection = new LdapConnection(identifier))
{
connection.Credential = new NetworkCredential(uid, pwd);
connection.AuthType = AuthType.Basic;
connection.SessionOptions.SecureSocketLayer = true;
connection.SessionOptions.ProtocolVersion = 3;
string filter = "(uid=" + username + ")";
SearchRequest request = new SearchRequest("ou=People,o=byu.edu", filter, SearchScope.Subtree);
Stopwatch sw = Stopwatch.StartNew();
SearchResponse response = connection.SendRequest(request) as SearchResponse;
sw.Stop();
Debug.WriteLine(sw.ElapsedMilliseconds);
foreach (SearchResultEntry entry in response.Entries)
{
Debug.WriteLine(entry.DistinguishedName);
foreach (System.Collections.DictionaryEntry attribute in entry.Attributes)
{
Debug.WriteLine(attribute.Key + " " + attribute.Value.GetType().ToString());
}
Debug.WriteLine("");
}
}
}
catch
{
Debugger.Break();
}
Debugger.Break();
return null; //debug
}
The PHP version of this follows this sequence:
Bind anonymously and look up the user information using a basedn and cn
Bind again using the username and password of the user to see if they are authentic
It does two binds (connects?) in 1/8th the time it takes the .NET version to do one! Its this sort of thing that makes me thing I am missing something.
I have tried methods based on the following sites:
http://roadha.us/2013/04/ldap-authentication-with-c-sharp/ - Required 2 queries to do what I wanted and was too slow. I went through probably 6 different tries of doing it differently (varying the authentication & connection settings, etc).
http://web.byu.edu/docs/ldap-authentication-0 - One PHP version, but has a small snippet about .NET at the bottom. I needed to get the profile as well and they weren't exactly descriptive.
System.DirectoryServices is slow? - Current version
EDIT:
Using wireshark, I saw that the following requests are made:
bindRequest passing along my uid (delta 0.7ms)
bindResponse success (delta 2ms)
searchRequest "ou=People,o=byu.edu" wholdSubtree (delta 0.2ms)
searchResEntry "uid=my uid,ou=People,o=byu.edu" | searchResDone success 1 result (delta 10.8ms)
unbindRequest (delta 55.7ms)
Clearly, the overhead is coming from .NET and not from the requests. These don't add up to 4.5 seconds in any way, shape, or form.
ldap.byu.edu sure looks like a fully qualified DNS host name. You should change your LdapDirectoryIdentifier constructor to new LdapDirectoryIdentifier("ldap.byu.edu", 636, true, false).
I think you're definitely on the right track using System.DirectoryServices for this, so you may just need to tweak your search request a bit.
You're only looking to get one result back here, correct? Set your size accordingly :
request.SizeLimit = 1;
This is a tricky one, but make also sure you're suppressing referral binds as well. You'll want to set this before you call connection.SendRequest(request) :
//Setting the DomainScope will suppress referral binds from occurring during the search
SearchOptionsControl SuppressReferrals = new SearchOptionsControl(SearchOption.DomainScope);
request.Controls.Add(SuppressReferrals);
The Problem: When new IIS Application Pools are created and set to use the Application Pool Identity for permissions, I am unsure how to add those identities to User Groups such as Administrator or Performance Counter Users.
The Background: I'm currently writing a C#.NET library which uses Microsoft.Web.Administration in order to do the following:
Detect if IIS 7.x is installed, and if so, what components.
Install or upgrade IIS 7.x to a provided list of required components.
Create/manage one or more web sites through IIS.
Automatically create/manage one application pool per web site
The context is that this library is to be used by executable installers to provide automated deployment of a web server and web sites/services on Windows Server OSes as part of a larger software deployment. So far, all of the above has been implemented, tested, and is (mostly) functional except for the automation of some permissions that need to be performed on Application Pool / Website creation.
In my method for installing a new website, I create a new Application Pool and force it to use the Application Pool Identity:
static public void InstallSite(string name, string path, int port)
{
Site site;
var appPoolName = ApplicationPoolBaseName + name;
using (var iisManager = new ServerManager())
{
// Set up a custom application pool for any site we run.
if (!iisManager.ApplicationPools.Any(pool => pool.Name.Equals(appPoolName)))
{
iisManager.ApplicationPools.Add(appPoolName);
iisManager.ApplicationPools[appPoolName].ManagedRuntimeVersion = "v4.0";
}
iisManager.CommitChanges();
}
// ... other code here ('site' gets initialized) ...
using (var iisManager = new ServerManager())
{
// Set anonymous auth appropriately
var config = iisManager.GetWebConfiguration(site.Name);
var auth = config.GetSection("system.web/authentication");
auth.SetMetadata("mode", "Windows");
var authSection = config.GetSection("system.webServer/security/authentication/anonymousAuthentication");
authSection.SetAttributeValue("enabled", true);
authSection.SetAttributeValue("userName", string.Empty); // Forces the use of the Pool's Identity.
authSection = config.GetSection("system.webServer/security/authentication/basicAuthentication");
authSection.SetAttributeValue("enabled", false);
authSection = config.GetSection("system.webServer/security/authentication/digestAuthentication");
authSection.SetAttributeValue("enabled", false);
authSection = config.GetSection("system.webServer/security/authentication/windowsAuthentication");
authSection.SetAttributeValue("enabled", false);
iisManager.CommitChanges();
}
// ... other code here ...
}
As I understand it, this would be the best security practice, and I would then add permissions to specific web sites for anything more than minimal system access. Part of this process would be to add these Application Pool identities to User Groups, such as Administrator or Performance Monitor Users. This is where complications arise.
Now, as documented elsewhere, each Application Pool Identity exists in the format of IIS AppPool\\<pool_name> but this faux-user is not listed through the normal GUI user management controls, and does not seem to be accessible through libraries such as System.DirectoryServices.AccountManagement when following this example on SO. Also, other questions about the Application Pool Identity seem to relate to referencing it from within a child website, not from within an installation context.
So, does anyone know what the proper methods are for
a) Referencing and accessing Application Pool Identities programmatically.
b) Giving Application Pool Identities permissions by adding them User Groups.
Thanks for your well-written question. It is exactly the problem that I was trying to solve last night and it gave me enough to go on that I was able finally cobble together an answer that uses only managed code. There were three steps that I found to getting the framework to find and work with the virtual user:
using new System.Security.Principal.NTAccount(#"IIS APPPOOL\<appPoolName>") to get a handle on the account.
using .Translate(typeof (System.Security.Principal.SecurityIdentifier)) to convert it to a SID
understanding that Principal.FindByIdentity() treats that SID like it is a group, rather than a user
A final working program (Windows Server 2012 for my test) is as follows:
using System;
using System.DirectoryServices.AccountManagement;
namespace WebAdminTest
{
internal class Program
{
private static void Main(string[] args)
{
var user = new System.Security.Principal.NTAccount(#"IIS APPPOOL\10e6c294-9836-44a9-af54-207385846ebf");
var sid = user.Translate(typeof (System.Security.Principal.SecurityIdentifier));
var ctx = new PrincipalContext(ContextType.Machine);
// This is weird - the user SID resolves to a group prinicpal, but it works that way.
var appPoolIdentityGroupPrincipal = GroupPrincipal.FindByIdentity(ctx, IdentityType.Sid, sid.Value);
Console.WriteLine(appPoolIdentityGroupPrincipal.Name);
Console.WriteLine(appPoolIdentityGroupPrincipal.DisplayName);
GroupPrincipal targetGroupPrincipal = GroupPrincipal.FindByIdentity(ctx, "Performance Monitor Users");
// Making appPoolIdentity "group" a member of the "Performance Monitor Users Group"
targetGroupPrincipal.Members.Add(appPoolIdentityGroupPrincipal);
targetGroupPrincipal.Save();
Console.WriteLine("DONE!");
Console.ReadKey();
}
}
}
A solution presented itself sooner than I expected, though it's not the one I preferred. For anyone interested, there are a couple of additional options on this pinvoke page. The managed solution did not work for me, but the sample using DllImport worked. I ended up adjusting the sample to handle arbitrary groups based on mapping an enum to SID strings, and including another DllImport for:
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool ConvertStringSidToSid(
string StringSid,
out IntPtr ptrSid);
The modified (working) function looks something like this:
static public bool AddUserToGroup(string user, UserGroup group)
{
var name = new StringBuilder(512);
var nameSize = (uint)name.Capacity;
var refDomainName = new StringBuilder(512);
var refDomainNameSize = (uint)refDomainName.Capacity;
var sid = new IntPtr();
switch (group)
{
case UserGroup.PerformanceMonitorUsers:
ConvertStringSidToSid("S-1-5-32-558", out sid);
break;
case UserGroup.Administrators:
ConvertStringSidToSid("S-1-5-32-544", out sid);
break;
// Add additional Group/cases here.
}
// Find the user and populate our local variables.
SID_NAME_USE sidType;
if (!LookupAccountSid(null, sid, name, ref nameSize,
refDomainName, ref refDomainNameSize, out sidType))
return false;
LOCALGROUP_MEMBERS_INFO_3 info;
info.Domain = user;
// Add the user to the group.
var val = NetLocalGroupAddMembers(null, name.ToString(), 3, ref info, 1);
// If the user is in the group, success!
return val.Equals(SUCCESS) || val.Equals(ERROR_MEMBER_IN_ALIAS);
}
Hopefully this will be of interest to someone else, and I would still like to know if anyone comes across a working, fully managed solution.
I have this piece of code in a program, to query what groups a Windows-domain user belongs to.
public void GetGroupNames(string userName, List<string> result)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain))
{
UserPrincipal uPrincipal = UserPrincipal.FindByIdentity(pc, userName);
if (uPrincipal != null)
{
PrincipalSearchResult<Principal> srcList = uPrincipal.GetGroups();
foreach (Principal item in srcList)
{
result.Add(item.ToString());
}
}
}
}
When I just implemented it and was debugging it,
UserPrincipal uPrincipal = UserPrincipal.FindByIdentity(pc, userName);
always got null.
I then had to close visual studio to do something else. When I came back, opened up visual studio, this code just worked. A few days ago, there was a network problem in the organisation, I did not switch off my PC during that period. After the network went back to normal, I could connect to internet OK, I could remote desktop to servers etc, which proves that Active Directory authentication was done all right, but the above piece of code failed to find UserPrinical for a given name, e.g. my own. I then reboot the PC, the code worked fine. I am quite puzzled regarding this matter. Is anyone able to provide a good explanation for this??
UserPrincipal has some known bugs with in cross domain scenarios. When it happens again, look and see if you can resolve groups from your machine. I have also encountered problems when unresolvable SIDs where group members.
I have a WCF service, which works if I use one login, but throws the following error if I try logging in with any other login. Strangely enough, if I change the password to the working login, the new password doesn't work but the old one still does. It's almost like it is caching something.
The error I get is this:
Multiple connections to a server or shared resource by the same user,
using more than one user name, are not allowed. Disconnect all
previous connections to the server or shared resource and try again
The code that causes the error is this:
public UserModel Login(string username, string password)
{
if (username == null || password == null)
return null;
using (var pContext = new PrincipalContext(ContextType.Machine))
{
if (pContext.ValidateCredentials(username, password))
{
using (var context = new MyEntities())
{
// I can tell from a SQL trace that this piece never gets hit
var user = (from u in context.Users
where u.LoginName.ToUpper() == username.ToUpper()
&& u.IsActive == true
select u).FirstOrDefault();
if (user == null)
return null;
var userModel = Mapper.Map<User, UserModel>(user);
userModel.Token = Guid.NewGuid();
userModel.LastActivity = DateTime.Now;
authenticatedUsers.Add(userModel);
sessionTimer.Start();
return userModel;
}
}
}
return null;
}
I see a related question here, which suggests the problem is with the PrincipalContext, but no answer
Update
Got it working..... I restarted our production server because we needed to have this working for someone important within the next hour, and I thought since it that previous link suggested that a reboot would get a single login in that I would just reboot and login with the login needed to get it working for now, and after rebooting everything works absolutely perfectly. I spent most of yesterday, staying late, and all of this morning trying to figure this out. We're not supposed to reboot our web server, but it was important to get this working so I did it anyways, and now everything works the way it should.
I would still like to know what its problem was though. My best guess is that something caused the PrincipalContext to not dispose correctly, which was preventing me from logging in with any other set of credentials.
Restarting the server fixed the issue, although I'd still love to know what the problem was.
My best guess is that something caused the PrincipalContext to not dispose correctly, which was preventing me from logging in with any other set of credentials.