.GetDirectoryEntry throws COM exception, code:0x800720720 when attempting to bind to object - asp.net

My application is running on IIS 7.0, it is supposed to impersonate the authenticated user and unlock or reset other user accounts. It worked fine when I was developing it on my workstation, but when I uploaded it to the server the impersonation stopped working, it won't bind to the AD objects and keeps throwing the same exception. I had the same problem earlier with using PrincipalContext but I was able to get around that using using(HostingEnvironment.Impersonate()) because I didn't need the authenticated user for that action. But now I do so I can't use that workaround. I need an actual fix for the issue and I would really appreciate some input. I've been searching far and wide for a solution to the problem and so far none of them have worked. Here is the code I'm using that keeps throwing the exception.
using (DirectorySearcher search = new DirectorySearcher(directoryEntries[counter]))
{
//Sets the filter to find a user object with the target user username
search.Filter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
//Sets the searchscope to search the whole AD
search.SearchScope = SearchScope.Subtree;
//Executes the search for one result and stores it in result.
SearchResult result = search.FindOne();
//Creates a directory entry from the result.
using (DirectoryEntry targetUser = result.GetDirectoryEntry())
{
//This if-else statement checks if the user is locked, if it is then
//the unlock is performed, and the unlockPerformed variable is set to
//true, if it isn't then unlockPerformed is set to false.
if (Convert.ToBoolean(targetUser.InvokeGet("IsAccountLocked")))
{
targetUser.InvokeSet("IsAccountLocked", false);
targetUser.CommitChanges();
unlockPerformed = true;
}
else
{
unlockPerformed = false;
}
}
}
This code worked perfectly before I uploaded it, any suggestions are greatly appreciated, I'll be monitoring this so I can get a fix asap.
Thanks in advance.
UPDATE: FIXED THE ISSUE
Apparently the program running from the hosting machine but not from a remote machine is actually a very telling symptom according to this article: http://msdn.microsoft.com/en-us/library/vstudio/ms730088(v=vs.100).aspx .
According to that article, the problem is that the impersonate setting was set to impersonation which causes this behaviour, I wanted DELEGATION. In order to do this I used this page to get information on different methods of delegation and impersonation, I used the "Impersonating Original Caller Temporarily" section.
In the web.config file:
<identity impersonate="false"/>
If this is set to false then it tries to impersonate the user for every action, which can cause issues like the one I was experiencing and isn't what I was trying to achieve.
In the code:
using System.Security.Principal;
...
// Obtain the authenticated user's Identity
WindowsIdentity winId = (WindowsIdentity)HttpContext.Current.User.Identity;
WindowsImpersonationContext ctx = null;
try
{
// Start impersonating
ctx = winId.Impersonate();
// Now impersonating
// Access resources using the identity of the authenticated user
}
// Prevent exceptions from propagating
catch
{
}
finally
{
// Revert impersonation
if (ctx != null)
ctx.Undo();
}
// Back to running under the default ASP.NET process identity
This fix is fairly easy, but its damn near impossible to find good clear information on this topic, I hope someone will find this useful some day, I can't be the only one experiencing these issues.

Apparently the program running from the hosting machine but not from a remote machine is actually a very telling symptom according to this article: http://msdn.microsoft.com/en-us/library/vstudio/ms730088(v=vs.100).aspx . According to that article, the problem is that the impersonate setting was set to impersonation which causes this behaviour, I wanted DELEGATION. In order to do this I used this page to get information on different methods of delegation and impersonation, I used the "Impersonating Original Caller Temporarily" section.
In the web.config file:
<identity impersonate="false"/>
If this is set to false then it tries to impersonate the user for every action, which can cause issues like the one I was experiencing and isn't what I was trying to achieve.
In the code:
using System.Security.Principal;
...
// Obtain the authenticated user's Identity
WindowsIdentity winId = (WindowsIdentity)HttpContext.Current.User.Identity;
WindowsImpersonationContext ctx = null;
try
{
// Start impersonating
ctx = winId.Impersonate();
// Now impersonating
// Access resources using the identity of the authenticated user
}
// Prevent exceptions from propagating
catch
{
}
finally
{
// Revert impersonation
if (ctx != null)
ctx.Undo();
}
// Back to running under the default ASP.NET process identity
This fix is fairly easy, but its damn near impossible to find good clear information on this topic, I hope someone will find this useful some day, I can't be the only one experiencing these issues.

Related

AAD Graph API returns 404 after call to AcquireTokenAsync, but not after call to AcquireTokenSilentAsync

So I have an application that's calling the graph API.
See the following snippet:
When the try (AquireTokenSilent) is called, the Web Request completes successfully no problem.
However, when make the same web request with the token I get from AcquireTokenAsync, I'm getting a 404 error and an exception thrown.
1) Can you recommend some good tools to analyze the HTTP request (so I can compare the difference and identify a problem). The Visual Studio debugger is helpful, but I can't see the whole picture, which obfuscates a possible problem here.
2) Can you help me identify why exactly one is successful and one fails? Both tokens seem to be acquired successfully, so I'm not sure what the problem is.
So I have figured out what was the underlying cause here.
In our authentication scenario, we have multiple products using azure AD SSO in the ecosystem. Since 'OnAuthorizationCodeReceived' is only called on login, and NOT when a valid login cookie is already held, the token cache will not be populated with the authorization code. So in this case, the Microsoft code samples for this scenario are dead wrong. Issuing an authentication challenge wont cause 'OnAuthorizationCodeReceived' to be called, as you already hold a valid login token.
So, while it's a litte ugly, the fix is dead simple. Force a logout, so that the token cache can be populated.
catch (AdalSilentTokenAcquisitionException e)
{
//in this case, it's possible there's no authorization code because the login cookie is from another session in
//the ecosystem. So in this scenario, force a logout so we can get a token into the tokencache
context.GetOwinContext().Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
sessionState.Abandon();
}
Now, because we're using this code outside the Controllers, and we call await, HttpContext will be null. Some serious voodoo going on in HttpContext, but I digress. We can use this little workaround to hang onto the context:
var context = HttpContext.Current;
var sessionState = context.Session;
EDIT: Came across one more problem when deploying the application to an azure app-service. You want to make sure Azure AD Authentication is toggle on in the 'Authentication' panel on Azure. We had some infinite login loop problems until I toggled this.
EDIT:
So, forcing a logout in this scenario really didn't sit well with me. BUT, I came across this question:
Azure Active Directory Graph API - access token for signed in user
And what we can do is follow the answer, and call AcquireTokenByAuthorizationCodeAsync(...), and make sure to use the 4 parameter method overload, where the last param is "https://graph.windows.net/"
Now, as long as we store the authorization code somewhere (in my case stored in a DB table). We should be able to get the authorization code for a given user, and get a new GraphAPI token, in the case where AcquireTokenSilentAsync(...) fails.
Now your statefull tokencache can be backed up by a stateless database call!
catch (AdalSilentTokenAcquisitionException e)
{
//in this case, the stateful cache is empty, so lets get the codeId from the DB
PersistentTokenCache pt = db.PersistentTokenCaches.Find(userObjectId);
if (pt != null && pt.token != null)
{
try
{
result = await ath.AcquireTokenByAuthorizationCodeAsync(pt.token,
new Uri(Startup.hostUri),
cc,
"https://graph.windows.net");
}
catch (AdalException ex)
{
Debug.WriteLine(ex.StackTrace);
//both authentication types have failed
pt.token = null;
await db.SaveChangesAsync();
context.GetOwinContext().Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
sessionState.Abandon();
return -1;
}
}
}

Setting ASP.Net Permissions - Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))

I have an MVC 4 application that allows a user to change their Active Directory password through a password reset functionality page. I have the following piece of code that sets the new password:
DirectoryEntry de = sr.GetDirectoryEntry();
de.Invoke("SetPassword", new object[] { newPassword });
de.Properties["LockOutTime"].Value = 0;
Upon trying to submit the form with the new password details I am having the following error written to the application event log:
0x80070005 (E_ACCESSDENIED))
I have set the Identity property of the Application Pool to NetworkService and had thought that this would have resolved the issue of connecting. Is there anything else further I need to ensure so that my ASPNET application can connect to the AD.
tl;dr
In our case, this started happening randomly. Turns out it's because our self-signed SSL certificate had expired. After creating a new one in IIS, the problem was resolved.
Explanation
This thread lead me to the cause.
I will briefly recap what SetPassword does here so you can see why you need it. That particular ADSI method really bundles 3 methods under the covers. It first tries to set the password over a secure SSL channel using LDAP. Next, it tries to set using Kerberos set password protocol. Finally, it uses NetUserSetInfo to attempt to set it.
The main problem is that only the first two methods will generally respect the credentials that you put on the DirectoryEntry. If you provide the proper credentials and an SSL channel for instance, the LDAP change password mechanism will work with those credentials...
If you check the NetUserSetInfo method, you will notice there is no place to put a username/password for authorization. In other words, it can only use the unmanaged thread's security context. This means that in order for it to work, it would have to impersonate the username/password combination you provided programmatically first...
LDAP over SSL is apparently the best way to go (and was the method that we had been using), and it appears (clarifications welcome) that once our self-signed SSL certificate had expired, it skipped Kerberos and fell back to NetUserSetInfo, which failed because it was not using the credentials we provided. (Or it just failed on Kerberos, as the poster said he had never seen the credentials passed in for Kerberos)
So after creating a new self-signed certificate (for COMPUTER.DOMAIN.local), the problem was resolved.
Here is the code (in case anyone's looking for it):
DirectoryEntry myDE = new DirectoryEntry(#"LDAP://OU=GroupName,DC=DOMAIN,DC=local");
myDE.Username = "administrator";
myDE.Password = "adminPassword";
DirectoryEntries myEntries = myDE.Children;
DirectoryEntry myDEUser = myEntries.Find("CN=UserName");
myDEUser.Invoke("SetPassword", new object[] { "NewPassword" });
myDEUser.Properties["LockOutTime"].Value = 0;
// the following 2 lines are free =)
myDEUser.Properties["userAccountControl"].Value = (int)myDEUser.Properties["userAccountControl"].Value | 0x10000; // don't expire password
myDEUser.Properties["userAccountControl"].Value = (int)myDEUser.Properties["userAccountControl"].Value & ~0x0002; // ensure account is enabled
myDEUser.CommitChanges();

User can log in without supplying password on localhost

I have a Asp.net web site built on C# with Forms Authentication. We use an Active Directory to authenticate the users, and everything works fine. But today we realized that it's possible to login to any account by just entering the username and click Login, without supplying any password! This is only happening on the development environment running on localhost (thank god!), but I don't like it...
I've never seen this behaviour before, and would really like someone to explain how this could happen. Is this a developer feature built by Microsoft? Or did someone at my office make a backdoor without telling the rest? I will investigate this last option further, but until then - have anyone encountered this before?
Big thanks in advance!
EDIT:
This is where the authentication returns true for every username I throw at it - with a blank password. Other passwords return false.
using (var context = new PrincipalContext(ContextType.Domain))
{
result = context.ValidateCredentials(username, password);
}
PrincipalContext is the default from System.DirectoryServices.AccountManagement
After some more investigation I found this on MSDN which states:
The ValidateCredentials method binds to the server specified in the constructor. If the username and password parameters are null, the credentials specified in the constructor are validated. If no credential were specified in the constructor, and the username and password parameters are null, this method validates the default credentials for the current principal.
and together with this information in the documentation of the constructor of PrincipalContext:
public PrincipalContext(System.DirectoryServices.AccountManagement.ContextType contextType, string name):
contextType: A System.DirectoryServices.AccountManagement.ContextType enumeration value specifying the type of store for the principal context.
name: The name of the domain or server for System.DirectoryServices.AccountManagement.ContextType.Domain context types, the machine name for System.DirectoryServices.AccountManagement.ContextType.Machine context types, or the name of the server and port hosting the System.DirectoryServices.AccountManagement.ContextType.ApplicationDirectory instance. If the name is null for a System.DirectoryServices.AccountManagement.ContextType.Domain context type this context is a domain controller for the domain of the user principal under which the thread is running. If the name is null for a System.DirectoryServices.AccountManagement.ContextType.Machine context type, this is the local machine name. This parameter cannot be null for System.DirectoryServices.AccountManagement.ContextType.ApplicationDirectory context types.
This leads me to conclude that since I don't use the name property in the constructor of the PrincipalContext, the domain controller will run under my own principal when on my dev machine. This could mean that it uses my users priveliges, which of course are much higher than the machine accounts the production servers are running as. This in turn could make all calls to Validate with nullas password automatically validate due to the higher level of privelige.
At least, this is my theory... Comments and thoughts are welcome, I will be closing this question soon.
Sounds like the problem is in the code. For a user to be AD authed the password needs to match. A security tokens is generated from AD, and this can't be done without the proper password or impersination (which also requires password).
Is the code using SELECT user FROM users WHERE password LIKE '%password%'? I've seen that done before! :(
Why don't you add null validation for password before calling ValidateCredentials? On a side note, client side authentication might help as well.
Why not you try to
handle NULL Parameter Exception.
check first both credentials have values.
using (var context = new PrincipalContext(ContextType.Domain))
{
if (string.IsNullOrEmpty(UserName) && string.IsNullOrEmpty(Password))
{
throw new ArgumentNullException();
result = null; // Or redirect to Login Page
}
else
{
result = context.ValidateCredentials(username, password);
}
}

SessionID changing across different instances in Azure (and probably in a web farm)

I have a problem with an Azure project with one WebRole but multiple instances that uses cookieless sessions. The application doesn't need Session storage, so it's not using any session storage provider, but I need to track the SessionID. Apparently, the SessionID should be the same accross the WebRole instances, but it changes suddently w/o explanation. We are using the SessionID to track some data, so it's very important.
In order to reproduce the issue:
Create a Cloud Project.
Add a ASP.NET Web Role. The code already in it will do.
Open Default.aspx
Add a control to see the current SessionID and a button to cause a postback
<p><%= Session.SessionID %></p>
<asp:Button ID="Button1" runat="server" Text="PostBack" onclick="Button1_Click" />
Add a event handler for button that will delay the response a bit:
protected void Button1_Click(object sender, EventArgs e)
{
System.Threading.Thread.Sleep(150);
}
Open Web.Config
Enable cookieless sessions:
<system.web>
<sessionState cookieless="true" />
</system.web>
Run the project, and hit fast and repeteadly the "PostBack" button for a while giving attention to the session id in the address bar. Nothing happens, the session id is always the same :). Stop it.
Open ServiceConfiguration.csfg
Enable four instances:
<Instances count="4" />
Ensure that in the Web.config there is a line related with the machine key that has been added automatically by Visual Studio. (at the end of system.web).
Rerun the project, hit fast and repeteadly the "Postback" button for a while and give attention to the session id in the address bar. You'll see how the SessionID changes after a while.
Why is this happening? As far as I know, if all machines share the machineKey, the session should be the same across them. With cookies there are no problems, the issue apparently is just when cookieless sessions are used.
My best guess, is that something wrong is happening when there are several instances, when the SessionID generated in one WebRole goes to another, is rejected and regenerated. That doesn't make sense, as all the WebRoles have the same machineKey.
In order to find out the problem, and see it more clearly, I created my own SessionIDManager:
public class MySessionIDManager : SessionIDManager
{
public override string CreateSessionID(HttpContext context)
{
if (context.Items.Contains("AspCookielessSession"))
{
String formerSessionID = context.Items["AspCookielessSession"].ToString();
// if (!String.IsNullOrWhiteSpace(formerSessionID) && formerSessionID != base.CreateSessionID(context))
// Debugger.Break();
return formerSessionID;
}
else
{
return base.CreateSessionID(context);
}
}
}
And to use it change this line in the WebConfig:
<sessionState cookieless="true" sessionIDManagerType="WebRole1.MySessionIDManager" />
Now you can see that the SessionID doesn't change, no matter how fast and for how long you hit. If you uncomment those two lines, you will see how ASP.NET is creating a new sessionID even when there is already one.
In order to force ASP.NET to create a new session, just a redirect to an absolute URL in your site:
Response.Redirect(Request.Url.AbsoluteUri.Replace(Request.Url.AbsolutePath, String.Empty));
Why is this thing happening with cookieless sessions?
How reliable is my solution in MySessionIDManager ?
Kind regards.
UPDATE:
I've tried this workaround:
User-Specified Machine Keys
Overwritten by Site-Level Auto
Configuration, but the problem
still stands.
public override bool OnStart()
{
// For information on handling configuration changes
// see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.
using (var server = new ServerManager())
{
try
{
// get the site's web configuration
var siteNameFromServiceModel = "Web"; // update this site name for your site.
var siteName =
string.Format("{0}_{1}", RoleEnvironment.CurrentRoleInstance.Id, siteNameFromServiceModel);
var siteConfig = server.Sites[siteName].GetWebConfiguration();
// get the appSettings section
var appSettings = siteConfig.GetSection("appSettings").GetCollection()
.ToDictionary(e => (string)e["key"], e => (string)e["value"]);
// reconfigure the machine key
var machineKeySection = siteConfig.GetSection("system.web/machineKey");
machineKeySection.SetAttributeValue("validationKey", appSettings["validationKey"]);
machineKeySection.SetAttributeValue("validation", appSettings["validation"]);
machineKeySection.SetAttributeValue("decryptionKey", appSettings["decryptionKey"]);
machineKeySection.SetAttributeValue("decryption", appSettings["decryption"]);
server.CommitChanges();
_init = true;
}
catch
{
}
}
return base.OnStart();
}
I've also tried this about put a
session start handler and add
some data, but no luck.
void Session_Start(object sender, EventArgs e)
{
Session.Add("dummyObject", "dummy");
}
Bounty up!
In short, unless you use cookies or a session provider there is no way for the session id to pass from one web role instance to the other. The post you mention says that the SessionID does NOT stay the same across web roles if you don't use cookies or session storage.
Check this previous question for ways to handle state storage in Azure, e.g. using Table Storage
The machineKey has nothing to do with sessions or the application domain, it is the key used to encrypt,decrypt,validate authentication and viewstate data. To verify this open SessionIDManager.CreateSessionID with Reflector. You will see that the ID value is just a random 16-byte value encoded as a string.
The AspCookielessSession value is already checked by SessionIDManager in the GetSessionID method, not CreateSessionID so the check is already finished before your code gets executed. Since the default sessionstate mode is InProc it makes sence that separate web roles will not be able to validate the session key so they create a new one.
In fact, a role may migrate to a different physical machine at any time, in which case its state will be lost. This post from the SQL Azure Team describes a way to use SQL Azure to store state for exactly this reason.
EDIT I finally got TableStorageSessionStateProvider to work in cookieless mode!
While TableStorageSessionStateProvider does support cookieless mode by overriding SessionStateStoreProviderBase.CreateUnititializedItem, it fails to handle empty sessions properly in private SessionStateStoreData GetSession(HttpContext context, string id, out bool locked, out TimeSpan lockAge,out object lockId, out SessionStateActions actions,bool exclusive). The solution is to return an empty SessionStateStoreData if no data is found in the underlying blob storage.
The method is 145 lines long so I won't paste it here. Search for the following code block
if (actions == SessionStateActions.InitializeItem)
{
// Return an empty SessionStateStoreData
result = new SessionStateStoreData(new SessionStateItemCollection(),
}
This block returns an empty session data object when a new session is created. Unfortunately the empty data object is not stored to the blob storage.
Replace the first line with the following line to make it return an empty object if the blob is empty:
if (actions == SessionStateActions.InitializeItem || stream.Length==0)
Long stroy short cookieles session state works as long as the provider supports it. You'll have to decide whether using cookieless state justifies using a sample provider though. Perhaps vtortola should check the AppFabric Caching CTP. It includes out-of-the-box ASP.NET providers, is a lot faster and it definitely has better support than the sample providers. There is even a step-by-step tutorial on how to set session state up with it.
Sounds tricky.
I have one suggestion/question for you. Don't know if it will help - but you sound like you're ready to try anything!
It sounds like maybe the session manager on the new machine is checking the central session storage provider and, when it finds that the session storage is empty, then it's issuing a new session key.
I think a solution may come from:
- using Session_Start as you have above in order to insert something into Session storage
- plus inserting a persistent Session storage provider of some description into the web.config - e.g. some of the oldest Azure samples provide a table based provider, or some of the newer samples provide an AppFabric caching solution.
I know your design is not using the session storage, but maybe you need to put something in (a bit like your Session_Start), plus you need to define something other than in-process session management.
Alternatively, you need to redesign your app around something other than ASP.NET sessions.
Hope that helps - good luck!
I experienced the same problem and after much research and debugging I found that the issue occurred because the "virtual servers" in the Azure SDK map the websites to different paths in the IIS metabase. (You can see this through through Request.ServerVariables["APPL_MD_PATH"].)
I just found this out now but wanted to post this so people could get working on testing it. My theory is that this problem may go away once it's published out to Azure proper. I'll update with any results I find.

Get the current WindowsPrincipal on a Forms authentication website

I'm coming across a peculiar request: I have a website that uses Forms Authentication, but it now needs to grab the WindowsPrincipal (Windows Authentication) at some point in order to reuse it.
My first instinct was to create a new page, and to disable Anonymous Access on IIS for that precise page. When I'm on that page, Request.ServerVariables["LOGON_USER"] gives me the current Windows login name as expected.
Unfortunately, Context.User still gives me a GenericPrincipal.
Any idea on how I could get the current WindowsPrincipal in a FormsAuthentication Application? (recreating it by asking the user for his password is not an option)
Found it: Context.Request.LogonUserIdentity is what should be used in this scenario.
It will return the windows user that made the request if Anonymous Access is disabled on IIS (otherwise it'll return the IIS anonymous user).
For those interested on how to reuse it:
lblUserName.Text = WindowsIdentity.GetCurrent().Name;
// will return the ASP.Net user (that's useless to us)
WindowsIdentity id = Context.Request.LogonUserIdentity;
WindowsImpersonationContext ctx = id.Impersonate();
try
{
lblUserName.Text = WindowsIdentity.GetCurrent().Name;
// will return the correct user
// (...) do your stuff
}
finally
{
ctx.Undo();
}

Resources