I am receiving an error a web based application that allows corporate intranet users to update their active directory details (phone numbers, etc).
The web application is hosted on IIS6 running Windows Server 2003 (SP1). The IIS website is using NTLM Authentication and the website has integrated security enabled. The IIS application pool runs using the “Network Service” account.
The web.config contains the following elements
<LdapConfigurations server="xxx.internal" root="OU=Staff Accounts,DC=xxx,DC=internal" domain="xxx" />
<identify impersonate=”true” />
Active Directory delegation is not needed as the following C# (.NET 3.5) code should pass on the correct impersonation details (including security token) onto Active Directory.
public void UpdateData(string bus, string bus2, string fax, string home, string home2, string mob, string pager, string notes)
{
WindowsIdentity windId = (WindowsIdentity)HttpContext.Current.User.Identity;
WindowsImpersonationContext ctx = null;
try
{
ctx = windId.Impersonate();
DirectorySearcher ds = new DirectorySearcher();
DirectoryEntry de = new DirectoryEntry();
ds.Filter = m_LdapUserFilter;
// i think this is the line causing the error
de.Path = ds.FindOne().Path;
this.AssignPropertyValue(bus, ADProperties.Business, ref de);
this.AssignPropertyValue(bus2, ADProperties.Business2, ref de);
this.AssignPropertyValue(fax, ADProperties.Fax, ref de);
this.AssignPropertyValue(home, ADProperties.Home, ref de);
this.AssignPropertyValue(home2, ADProperties.Home2, ref de);
this.AssignPropertyValue(mob, ADProperties.Mobile, ref de);
this.AssignPropertyValue(pager, ADProperties.Pager, ref de);
this.AssignPropertyValue(notes, ADProperties.Notes, ref de);
// this may also be causing the error?
de.CommitChanges();
}
finally
{
if (ctx != null)
{
ctx.Undo();
}
}
}
private void AssignPropertyValue(string number, string propertyName, ref DirectoryEntry de)
{
if (number.Length == 0 && de.Properties[propertyName].Value != null)
{
de.Properties[propertyName].Remove(de.Properties[propertyName].Value);
}
else if (number.Length != 0)
{
de.Properties[propertyName].Value = number;
}
}
User details can be retrieved from Active Directory without a problem however the issue arises when updating the users AD details. The following exception message is displayed.
System.Runtime.InteropServices.COMException (0x80072020): An operations error occurred.
at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
at System.DirectoryServices.DirectoryEntry.Bind()
at System.DirectoryServices.DirectoryEntry.get_AdsObject()
at System.DirectoryServices.DirectorySearcher.FindAll(Boolean findMoreThanOne)
at System.DirectoryServices.DirectorySearcher.FindOne()
at xxx.UpdateData(String bus, String bus2, String fax, String home, String home2, String mob, String pager, String notes)
at xxx._Default.btnUpdate_Click(Object sender, EventArgs e)
The code works fine in our development domain but not in our production domain. Can anyone please assist in helping resolving this problem?
This is more than likely a permissions problem - there are numerous articles regards impersonation and delegation and the vagaries thereof here: http://support.microsoft.com/default.aspx?scid=kb;en-us;329986 and here: http://support.microsoft.com/default.aspx?scid=kb;en-us;810572.
It sounds like you might have a duplicate SPN issue?
This is why I think it might be a problem:
It works in your dev enviroment (assuming it is also using network service, and on the same domain)
You have impersonate on, in your web config.
When there is a duplicate SPN, it invalidates the security token, so even though you have created it correctly in code, AD does not "trust" that server to impersonate, so the server that receives the request to make a change to AD (on of your DC's) receives the request but then discards it because Delagation permission has not been applied on the machine account in AD, or SPN issue (either duplicate or incorrect machine name / domain name)
Or at least in my expereince that is 9 out of 10 times the problem.
I guess the problem is that it works on the development environment because when you're launching your webapp there, you run it with your personal account which probably has the rights to write to AD.
On the production environment, you have to assure that the process running your webapp (Network Service Account) has also the rights to update the AD. It sounds to me like that could be the problem since I had a similar issue once.
The problem wasn't with the code but how the server was setup on the domain. For some reason the network administrator did not select the "Trust Computer for Delegation" option in active directory.
Happily the problem was not a "double-hop" issue :)
Related
This question is related to Starting processes under specific credentials from a Windows service, but it's a different problem.
I've started a process from a Windows service in the System session (0) under specific credentials, but it is unable to listen to a port sharing URL. It's using a "Worker" domain account on a Windows Server 2008 machine.
My SMSvcHost.exe.config file: http://pastie.org/private/jxed8bdft0eir5uc371pq
I've restarted the services and the machine as well, but it's still giving me this exception:
System.ServiceModel.CommunicationException: The service endpoint failed to listen on the URI 'net.tcp://localhost:5400/Agent/384' because access was denied. Verify that the current user is granted access in the appropriate allowAccounts section of SMSvcHost.exe.config. ---> System.ComponentModel.Win32Exception: Access is denied
at System.ServiceModel.Activation.SharedMemory.Read(String name, String& content)
at System.ServiceModel.Channels.SharedConnectionListener.SharedListenerProxy.ReadEndpoint(String sharedMemoryName, String& listenerEndpoint)
My ProcessHelper code that starts the process: http://pastie.org/private/iytqehsdfujrgil1decda. I'm calling the StartAsUserFromService method.
I suppose the link between the SID in the config and the account the process is running under is somehow not being made. But why?
EDIT:
I've double-checked that the config I'm editing is used by the service. I've tried adding the System account and Everyone explicitly, but it's still giving me an access denied error. It's like it's not looking at that config at all.
How can I find where the missing permission is?
EDIT:
I reinstalled .NET 4.5.1 on the machine and all the Windows updates, still no luck.
EDIT:
Is this the correct way of duplicating a user token to allow it to use port sharing? Specifically the SecurityDescriptor bit?
private static ImpersonationResult ImpersonateUser(string domain, string username, string password)
{
IntPtr token = IntPtr.Zero;
IntPtr primaryToken = IntPtr.Zero;
try
{
// Get token
bool bImpersonated = LogonUser(
username,
domain,
password,
(int)LogonType.NetworkClearText,
(int)LogonProvider.Default,
ref token);
if (!bImpersonated)
{
throw new Exception(string.Format("Failed to impersonate identity. Error code: {0}", Marshal.GetLastWin32Error()));
}
SecurityDescriptor sd = new SecurityDescriptor();
IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(sd));
Marshal.StructureToPtr(sd, ptr, false);
InitializeSecurityDescriptor(ptr, 1);
sd = (SecurityDescriptor)Marshal.PtrToStructure(ptr, typeof(SecurityDescriptor));
// Set up security
bool bDecriptorSet = SetSecurityDescriptorDacl(
ref sd,
true,
IntPtr.Zero,
false);
if (!bDecriptorSet)
{
throw new Exception(string.Format("Failed to set security descriptor. Error code: {0}", Marshal.GetLastWin32Error()));
}
SecurityAttributes processAttributes = new SecurityAttributes();
processAttributes.lpSecurityDescriptor = ptr;
processAttributes.nLength = (uint)Marshal.SizeOf(sd);
processAttributes.bInheritHandle = true;
// Duplicate token
bool bTokenDuplicated = DuplicateTokenEx(
token,
0,
ref processAttributes,
(int)SecurityImpersonationLevel.SecurityImpersonation,
(int)TokenType.TokenPrimary,
ref primaryToken);
if (!bTokenDuplicated)
{
throw new Exception(string.Format("Failed to duplicate identity token. Error code: {0}", Marshal.GetLastWin32Error()));
}
SecurityAttributes threadAttributes = new SecurityAttributes();
threadAttributes.lpSecurityDescriptor = IntPtr.Zero;
threadAttributes.nLength = 0;
threadAttributes.bInheritHandle = false;
// Got the token
return new ImpersonationResult()
{
Token = primaryToken,
ProcessAttributes = processAttributes,
ThreadAttributes = threadAttributes
};
}
finally
{
FreeToken(token);
}
}
private static void FreeToken(IntPtr token)
{
if (token != IntPtr.Zero)
{
CloseHandle(token);
}
}
EDIT:
Here's the app.config bit of my process that enables port sharing: http://pastie.org/private/8ekqeps4d7rmo7hnktsw
Here's an app.config bit of the service that starts the process: http://pastie.org/private/nqqcwz8bvjb5fzp48yavbw. It has no problems using Port Sharing because it's running under the System account.
The Port Sharing service itself is enabled, and I already mentioned that I've restarted the machine several times.
The "Application Server" role is not installed, but when I go to add it, the TCP Port Sharing Role is already ticked and greyed out, so something else must have installed it. Does it come with .NET 4.5.1?
Preface: Please use for one question one thread...
PortSharing: WHERE have you enabled port sharing? We cannot see this in your configuration file. For more infos see: How to: Configure a Windows Communication Foundation Service to Use Port Sharing
Have you installed the "Application Server" role on your server? See also: Checklist: Use TCP Port Sharing to Allow Multiple WCF Applications to Use the Same TCP Port
Is port sharing enabled on the system? See: How to: Enable the Net.TCP Port Sharing Service
Also, have you restarted your server? Sometimes this is needed (or at least all services which uses this port), see: http://blogs.msdn.com/b/joncole/archive/2010/06/10/tcp-port-sharing-access-is-denied.aspx
A comprehensive description about port-sharing is: http://blogs.msdn.com/b/andreal/archive/2009/04/05/net-tcp-ip-port-sharing.aspx
Also be aware that you must add some accounts for activation, if this is needed:
Configuring the Net.TCP Port Sharing Service
Are you sure that the SmSvcHost.exe.config allows access from your account, your process is running under?
<configuration>
<system.serviceModel.activation>
<net.tcp listenBacklog="16" <!—16 * # of processors -->
maxPendingAccepts="4"<!— 4 * # of processors -->
maxPendingConnections="100"
receiveTimeout="00:00:30" <!—30 seconds -->
teredoEnabled="false">
<allowAccounts>
<!-- LocalSystem account -->
<add securityIdentifier="S-1-5-18"/>
<!-- LocalService account -->
<add securityIdentifier="S-1-5-19"/>
<!-- Administrators account -->
<add securityIdentifier="S-1-5-20"/>
<!-- Network Service account -->
<add securityIdentifier="S-1-5-32-544" />
<!-- IIS_IUSRS account (Vista only) -->
<add securityIdentifier="S-1-5-32-568"/>
</allowAccounts>
</net.tcp>
</system.serviceModel.activation>
It turns out that the logon type was causing the permissions to not work correctly with the Port Sharing Service. I changed it to LogonType.Batch and it started working.
Full code:
ProcessHelper: http://pastie.org/private/dlkytj8rbigs8ixwtg
TokenImpersonationContext: http://pastie.org/private/nu3pvpghoea6pwwlvjuq
(Just another answer that may help someone)
Turns out that in my case, logged in user did not have Administrative privilege.
Changing the account type to Administrator solved the problem.
I also did not change anything in SmSvcHost.exe.config
I have a WCF service which contains a Login method that validates a username and password against the local machine credentials, and after a seemingly random period of time it will stop working for some users.
The actual login command looks like this:
public UserModel Login(string username, string password, string ipAddress)
{
// Verify valid parameters
if (username == null || password == null)
return null;
try
{
using (var pContext = new PrincipalContext(ContextType.Machine))
{
// Authenticate against local machine
if (pContext.ValidateCredentials(username, password))
{
// Authenticate user against database
using (var context = new MyEntities(Connections.GetConnectionString()))
{
var user = (from u in context.Users
where u.LoginName.ToUpper() == username.ToUpper()
&& u.IsActive == true
&& (string.IsNullOrEmpty(u.AllowedIpAddresses)
|| u.AllowedIpAddresses.Contains(ipAddress))
select u).FirstOrDefault();
// If user failed to authenticate against database
if (user == null)
return null;
// Map entity object to return object and assign session token
var userModel = Mapper.Map<User, UserModel>(user);
userModel.Token = Guid.NewGuid();
userModel.LastActivity = DateTime.Now;
// Authenticated users are added to a list on the server
// and their login expires after 20 minutes of inactivity
authenticatedUsers.Add(userModel);
sessionTimer.Start();
// User successfully authenticated, so return UserModel to client
return userModel;
}
}
}
}
catch(Exception ex)
{
// This is getting hit
MessageLog.WriteMessage(string.Format("Exception occurred while validating user: {0}\n{1}", ex.Message, ex.StackTrace));
return null;
}
// If authentication against local machine failed, return null
return null;
}
This appears to work fine for a few days, then it will abruptly stop working for some users and throw this exception:
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. (Exception from HRESULT: 0x800704C3)
at System.DirectoryServices.AccountManagement.CredentialValidator.BindSam(String target, String userName, String password)
at System.DirectoryServices.AccountManagement.CredentialValidator.Validate(String userName, String password)
at System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials(String userName, String password)
at MyNamespace.LoginService.Login(String username, String password, String ipAddress) in C:\Users\me\Desktop\somefolder\LoginService.svc.cs:line 67
Line 67 is: if (pContext.ValidateCredentials(username, password))
I'm not sure if it matters or not, but the final line of the error message is the path of the VS solution on my development machine, not the path to the files on the production server.
When it fails, it only fails for some users, while others can continue to login just fine. The only thing I have found to temporarily fix the error is running iisreset. Stopping/starting the web site or recycling the app pool doesn't work.
I am unable to reproduce the error on demand. I've tried logging in with the same user from multiple sessions and IP addresses, logging in with different users from the same browser session at the same time, spamming the Login button to try and get it to run multiple times, etc but everything appears to work fine.
I can see from our logging that users have been successfully able to login in the past:
3/21/2013
o 9:03a I logged in
o 1:54p UserB logged in
o 1:55p UserA logged in
o 2:38p UserB logged in
o 3:48p UserB logged in
o 5:18p UserA logged in
o 6:11p UserB logged in
3/22/2013
o 12:42p UserA logged in
o 5:22p UserB logged in
o 8:04p UserB logged in
3/25/2013 (today)
o 8:47a I logged in
o 12:38p UserB tries logging in and fails. Repeated ~15 times over next 45 min
o 1:58p I login successfully
o 2:08p I try to login with UserB's login info and fail with the same error
The reason we authenticate against the local machine is because users have an account created locally for FTP access, and we didn't want to build our own custom login system or make our users remember two sets of credentials.
The code should only authenticate the user's credentials, and does not do do anything else with the user's credentials. There is no other code that uses System.DirectoryServices, no file IO going on, and no access to anything locally on the file system other than the files required to run the web application.
What can cause that error to appear, seemingly at random, after a few days? And how can I fix it?
The server is Windows Server 2003, which runs IIS 6.0, and it is setup to use .Net Framework 4.0
The closest I can find online towards explaining this problem is this forum post, where the user experiencing the same error and got a replay stating:
The WinNT provider does not do well in a server environment. I am
actually suprised you don't see this with a much smaller load. I have
been able to get this with only 2 or 3 users.
and this SO comment stating
The BEST way to correctly authenticate someone is to use LogonUserAPI
as #stephbu write. All other methods described in this post will NOT
WORK 100%
where "all other methods" includes the top voted answer of using PrincipalContext.ValidateCredentials
Its sounding like PrincipalContext.ValidateCredentials isn't completely 100% reliable on Windows Server 2003 and IIS6.0, so I rewrote my authentication code to use the LogonUser WinAPI method instead.
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out IntPtr phToken
);
IntPtr hToken;
if (LogonUser(username, "", password,
LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, out hToken))
{
...
}
I am having issues getting the groups from Active Directory via System.DirectoryServices
Originally I started my application on a computer that was registered on the domain, but as it was a live domain I did not want to do any writes to AD what so ever, so I set up a machine with Windows XP as the host operating system, and installed windows server 2003 on a VM.
I've added another Ethernet port in the machine and set up a switch, the 1 Ethernet port is dedicated to the VM and the other port is used for the host.
After configuring the IP addresses to get them communicating I transferred my application onto the host machine and fired it up, but I was getting an DirectoryServicesCOMException.
With the message that the user name and password was invalid :( just to check that it was not active directory I created a 3rd virtual machine and installed Windows XP, which i added to the domain with the credentials tested in the APP, works a treat.
So I thought it must be because the machine where the application is running is not part of the domain.
Heres the block of code that was causing the issue:
public CredentialValidation(String Domain, String Username, String Password, Boolean Secure)
{
//Validate the Domain!
try
{
PrincipalContext Context = new PrincipalContext(ContextType.Domain, Domain); //Throws Exception
_IsValidDomain = true;
//Test the user login
_IsValidLogin = Context.ValidateCredentials(Username, Password);
//Check the Group Admin is within this user
//******HERE
var Results = UserPrincipal.FindByIdentity(Context, Username).GetGroups(Context);
foreach(Principal Result in Results)
{
if (Result.SamAccountName == "Domain Admins")
{
_IsAdminGroup = true;
break;
}
}
Results.Dispose();
Context.Dispose();
}
catch (PrincipalServerDownException)
{
_IsValidDomain = false;
}
}
The information in the login dialogue is being entered like so:
Domain: test.internal
Username: testaccount
Password: Password01
Hope someone can shed some light in this error.
Update:
After checking the Security Logs on the server i can see that my log in attempts was successful, but this is down to:
_IsValidLogin = Context.ValidateCredentials(Username, Password);
The line after where im checking the groups is causing the error, so the main issue is that the lines of code below are not working correctly from a machine thats not joined to the network:
var Results = UserPrincipal.FindByIdentity(Context, Username).GetGroups(Context);
According to your code snippet, you're failing when you attempt to create the PrincipalContext, before calling ValidateCredentials. At that point the thread running your code is still working under either a local identity (if you're in a web process) or the identity you signed onto your machine with (for a windows process). Either of these won't exist on the test.internal domain.
You might want to try the overload of PrincipalContext that includes the username and password in the constructor. See http://msdn.microsoft.com/en-us/library/bb341016.aspx
I used to do quite a bit of user management via C# .NET. I just dug up some methods you can try.
The following two methods will get a DirectoryEntry object for a given SAM account name. It takes a DirectoryEntry that is the root of the OU you want to start searching for the account at.
The other will give you a list of distinguished names of the groups the user is a member of. You can then use those DN's to search AD and get a DirectoryEntry object.
public List<string> GetMemberOf(DirectoryEntry de)
{
List<string> memberof = new List<string>();
foreach (object oMember in de.Properties["memberOf"])
{
memberof.Add(oMember.ToString());
}
return memberof;
}
public DirectoryEntry GetObjectBySAM(string sam, DirectoryEntry root)
{
using (DirectorySearcher searcher = new DirectorySearcher(root, string.Format("(sAMAccountName={0})", sam)))
{
SearchResult sr = searcher.FindOne();
if (!(sr == null)) return sr.GetDirectoryEntry();
else
return null;
}
}
I have a WCF data service and I am trying to use the UpdateObject method on the DataServiceContext client. When I call the SaveChanges method, I get the following error page:
Unauthorized: Access is denied due to invalid credentials
You do not have permission to view this directory or page using the credentials that you supplied.
Does anyone have any ideas of how I can fix this? I found this, which would theoretically fix the problem, but setting this disk access is not a realistic solution for a production service. Keep in mind, when running this WCF Data service on my local machine, it works just fine. The C# code for my call is below:
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
var userName = ( string ) context[ "UserName" ];
var isAuthenticated = ( bool ) context[ "IsAuthenticated" ];
if (userName != null && userName.Length >= 1 && collection.Count >= 1)
{
var allNames = string.Empty;
var allValues = string.Empty;
byte[] buf = null;
PrepareDataForSaving( ref allNames, ref allValues, ref buf, true, collection, isAuthenticated );
if (allNames.Length != 0)
{
var client = GetDataContext( );
var profile = client.ProfileViews.Where(p => p.UserName == userName).FirstOrDefault();
if (profile == null)
{
profile = new ProfileView() { UserName = userName };
client.AddToProfileViews(profile);
}
profile.PropertyNames = allNames;
profile.PropertyValuesString = allValues;
profile.PropertyValuesBinary = buf;
profile.LastUpdateDate = DateTime.UtcNow;
client.UpdateObject(profile);
client.SaveChanges( );
}
}
}
I had a similiar issue with a Silverlight application that was using WCF Data Services. The users could view data in the application---SELECTS from the database were functioning---but they were receiving the same "401 - Unauthorized: Access is denied du to invalid credentials." you are describing when they attempted to save changes to the database (i.e., at the point when SaveChanges was executed in code). The underlying problem ended up being a permissions issue with the IIS application folder itself. To fix, we had to grant the Application Pool being used by the WCF service Write permissions to the folder.
Go to IIS and right click on the virtual directory containing the WCF service and choose Manage Application -> Advanced Settings.... Make note of the Application Pool name.
Within same right-click menu, choose Edit Permissions..... On Security tab, check group and user names.
If the name of the Application Pool is missing. Add it using the name format "IIS APPPOOL\MyApplicationPoolName". See this helpful link: http://www.bluevalleytech.com/techtalk/blog/assigning-ntfs-folder-permission-to-iis7-application-pools/
Make sure MyApplicationPoolName has Write permissions (we actually gave it Full control)
In my case I had Windows authentication with impersonation using kerberos (useAppPoolCredentials=true) on a WCF Data Service hosted on IIS 7.5. The weird thing was that I could successfully select and insert data, but when I tried to update I got a 401.3 access denied error. The solution was to give the AD group specified in the web config (myRole):
<authorization>
<allow roles="myRole"/>
<deny users="*" />
</authorization>
Read and write access to the application folder like Dan Sabin said . The error message was:
Error message 401.3: You do not have permission to view this directory or page using the credentials you supplied (access denied due to Access Control Lists). Ask the Web server's administrator to give you access
I need to change Logon user for a Windows service programmatically. And I am using the following code to do that:
string objPath = string.Format("Win32_Service.Name='{0}'", ServiceName);
using (ManagementObject service = new ManagementObject(new ManagementPath(objPath)))
{
object[] wmiParams = new object[11];
if (PredefinedAccount)
{
wmiParams[6] = "LocalSystem";
wmiParams[7] = "";
}
else
{
wmiParams[6] = ServiceUsername; // provided by user
wmiParams[7] = ServicePassword; // provided by user
}
object invokeResult = service.InvokeMethod("Change", wmiParams);
// handle invokeResult - no error up to this point
}
This code works in 90% of situations, but in some situations service cannot be started due to logon failure. There is usually no error on InvokeMetod but when we try to start the service we get the following error:
System.InvalidOperationException: Cannot start service X on computer
'.'. --> System.ComponentModel.Win32Exception: The service did not
start due to a logon failure.
The workaround solution is simple, we just need to enter the same credentials via Windows interface and problem is solved.
So my question is, has anybody experienced the similar problem with ManagementObject because it seems that in some situation it does not relate Username and password to windows service?
It's because the account has no "Log On as service" privilege. You need to use LsaAddAccountRights to add such privilege to the account.
Do you notice any patterns amongst those failures? Same machine? Same OS? Same user? Does the user have "logon as service" or "logon interactively" rights? Personally, I am not familiar with this method of specifying the user for a service. I would have thought you would have to restart the service, but I guess not if it works 90% of the time.