We have a particular SQL Server which we need to access from a thick (.Net 4.0 WPF) client, and the only credentials available to us for that connection is a 'service' account which is effectively an Active Directory account with permission on the SQL Server.
I am using Entity Framework, and ObjectContext, throughout the project, so am continuing with it here. After looking around, I have implemented an impersonation routine based on LogonUserEx, and DuplicateTokenEx, which allows me, via Dependency Injection, to write the following:
using (container.Resolve<Impersonate>())
using (var context = container.Resolve<MyObjectContext>())
{
context.Connection.Open();
//Do some work with the data as the service account.
context.Connection.Close();
}
The constructor of the Impersonate class above calls LogonUserEx and so on. I am explicitly opening and closing the connection as part of a Unit Of Work pattern, which shouldn't be relevant.
Now, via debugging I have found that the token is successfully retrieved for the service account and the user is 'impersonated'. However, as soon as I try to instantiate the ObjectContext I get the following error:
System.TypeInitializationException: The type initializer for 'EntityBid' threw a
n exception. ---> System.IO.FileLoadException: Could not load file or assembly '
System.Data.Entity.dll' or one of its dependencies. Either a required impersonat
ion level was not provided, or the provided impersonation level is invalid. (Exc
eption from HRESULT: 0x80070542)
at System.Runtime.InteropServices.Marshal.GetHINSTANCE(RuntimeModule m)
at System.Runtime.InteropServices.Marshal.GetHINSTANCE(Module m)
at EntityBid.initEntryPoint()
at EntityBid.internalInitialize()
at EntityBid..cctor()
--- End of inner exception stack trace ---
at EntityBid.Trace(String fmtPrintfW, String a1)
at System.Data.EntityUtil.ProviderExceptionWithMessage(String message, Except
ion inner)
at System.Data.EntityClient.EntityConnection.OpenStoreConnectionIf(Boolean op
enCondition, DbConnection storeConnectionToOpen, DbConnection originalConnection
, String exceptionCode, String attemptedOperation, Boolean& closeStoreConnection
OnFailure)
at System.Data.EntityClient.EntityConnection.Open()
at Core.Security.TestHarness._2.Class1..ctor()
Consequently, it would appear that the account I am impersonating no longer has access or sufficient privelage to load the DLL's from the GAC. The Fusion log is not giving any additional information.
I am not sure how to solve this. I wonder if I am not currently retrieving a token with sufficient privelage. Note that I am providing these paramteres to LogonUserEx: LOGON32_LOGON_NETWORK_CLEARTEXT and LOGON32_PROVIDER_DEFAULT.
Finally, note that this process works absolutely fine on a machine with administrative privelages for the logged on user. It breaks when I run it on a user with a 'normal' account, subject to the usual corporate GPO!
EDIT: Just to include the 'important' part of the impersonation. Notice that I have now swapped to LOGON32_LOGON_UNLOCK, as it is working better. Apologies for the slightly iffy formatting:
if (LogonUserEx(dUser, dDomain, dPassword, LOGON32_LOGON_UNLOCK,
LOGON32_PROVIDER_DEFAULT, out token, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero))
{
if (DuplicateTokenEx(token, MAXIMUM_ALLOWED, ref sa, SECURITY_IMPERSONATION_LEVEL.SecurityDelegation, TOKEN_TYPE.TokenPrimary, out tokenDuplicate))
{
m_ImpersonatedUser = new WindowsIdentity(token);
_windowsImpersonationContext = m_ImpersonatedUser.Impersonate();
Any help greatly appreciated.
Nick.
Related
I am attempting to obtain a Kerberos Token from a C# Application (not web-based, a standalone executable).
I have referred to this question to figure out how to do it, but even trying both answers, I get the same problem.
When I reach the GetToken line (using Furkat's answer as a reference here), I get an exception:
KerberosRequestorSecurityToken securityToken = tokenProvider.GetToken(TimeSpan.FromMinutes(1)) as KerberosRequestorSecurityToken;
Here is the exception and the nested innerExceptions:
Exception: The NetworkCredentials provided were unable to create a Kerberos credential, see inner exception for details.
innerException: Authenticating to a service running under a user account which requires Kerberos multilegs, is not supported.
innerException: The function completed successfully, but must be called again to complete the context
I have some serious problems trying to find any examples of this working for a non-web based application, the StackOverflow question I linked is pretty much the closest I've got to getting what I need.
I also have problems figuring out exactly how things are supposed to work, since I can't get an example to work on my side. I'm looking for some sort of unique token for the user, that can then be passed to a SAML POST call to a server for Single Sign On. What will this token look like? Is it right to use TokenImpersonationLevel.Impersonation, instead of Identification here? (Identification gives me the same problem).
So my question is about my error and how to fix it, but I would really appreciate an explanation with the answer, telling me about the context (what was going wrong, what I misunderstood, etc).
Here's my complete Method. It's in Proof-Of-Concept stage right now, so forgive the temporary bad naming and ugly code. I'm making lots of trial-and-error.
public string Method5()
{
try
{
var userName1 = new WindowsPrincipal(WindowsIdentity.GetCurrent()).Identity.Name;
var domainName = userName1.Split('\\').First();
var userName = userName1.Split('\\').Last();
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
var domain = Domain.GetCurrentDomain().ToString();
using (var domainContext = new PrincipalContext(ContextType.Domain, domain))
{
string spn = UserPrincipal.FindByIdentity(domainContext, IdentityType.SamAccountName, userName).UserPrincipalName;
KerberosSecurityTokenProvider tokenProvider = new KerberosSecurityTokenProvider(spn, TokenImpersonationLevel.Impersonation, CredentialCache.DefaultNetworkCredentials);
KerberosRequestorSecurityToken securityToken = tokenProvider.GetToken(TimeSpan.FromMinutes(1)) as KerberosRequestorSecurityToken;
string serviceToken = Convert.ToBase64String(securityToken.GetRequest());
return serviceToken;
}
}
catch (Exception ex)
{
return "Failure";
}
}
The error indicates that you are requesting a Kerberos User2User token. The multileg bit is correct, but somewhat misleading. The issue is that AD determines it's a U2U request and makes the API return a specific error, indicating it's U2U and requires a retry with different parameters. .NET doesn't understand this retry, hence the error.
The reason you're requesting a U2U token is because you're calling the token provider asking for it to request a token to access the given SPN, which in this case is just an ordinary user. This is generally not useful in client/server applications.
KerberosSecurityTokenProvider tokenProvider = new KerberosSecurityTokenProvider(spn, TokenImpersonationLevel.Impersonation, CredentialCache.DefaultNetworkCredentials);
What this code is doing is saying for a user that has been inferred by impersonation or authentication previously, request a token so that user can access a remote service {SPN}. A token is only useful for a single user to a single service. You can't just collect a token and user it everywhere. This is not how Kerberos-proper works. Kerberos determines the name of that service by the SPN. In this case it already knows who the caller is.
So, the correct solution is:
var identity = Thread.CurrentPrincipal.Identity; // domain\username1
var spn = "host/someservice.domain.com";
var tokenProvider = new KerberosSecurityTokenProvider(spn);
var securityToken = tokenProvider.GetToken(TimeSpan.FromMinutes(1)); // token for username1 to host/someservice.domain.com
I've inherited a google adwords project which is using OAuth 2.0 for Installed Applications to connect to the google adwords API.
I'm falling over on the first hurdle; I've downloaded the following example project from github;
https://github.com/googleads/googleads-dotnet-lib/tree/master/examples/AdWords/CSharp/OAuth
When I place all the details into the web.config as directed it doesn't work.
I've put in;
the developer token
the customer Id
The refresh token.
This is all brand new to me, my understanding is that when using the refresh token the application should get a new access token and allow access to the API.
The above image shows the sample app interface, when I click on the "Get campaigns" link receive an exception message;
Google.Api.Ads.Common.Lib.AdsOAuthException was caught
HResult=-2146233088
Message=Failed to refresh access token.
{
"error" : "invalid_client"
}
Source=Google.Ads.Common
StackTrace:
at Google.Api.Ads.Common.Lib.OAuth2ProviderForApplications.RefreshAccessTokenInOfflineMode()
at Google.Api.Ads.Common.Lib.OAuth2ProviderForApplications.RefreshAccessToken()
at Google.Api.Ads.Common.Lib.OAuth2ProviderBase.RefreshAccessTokenIfExpiring()
at Google.Api.Ads.Common.Lib.OAuth2ProviderForApplications.RefreshAccessTokenIfExpiring()
at Google.Api.Ads.Common.Lib.OAuth2ProviderBase.GetAuthHeader()
at Google.Api.Ads.Common.Lib.OAuth2ProviderForApplications.GetAuthHeader()
at Google.Api.Ads.AdWords.Lib.AdWordsSoapClient.InitForCall(String methodName, Object[] parameters)
at Google.Api.Ads.Common.Lib.AdsSoapClient.MakeApiCall(String methodName, Object[] parameters)
at Google.Api.Ads.Common.Lib.AdsSoapClient.Invoke(String methodName, Object[] parameters)
at Google.Api.Ads.AdWords.v201601.CampaignService.get(Selector serviceSelector)
at Google.Api.Ads.AdWords.Examples.CSharp.OAuth.Default.OnGetCampaignsButtonClick(Object sender, EventArgs eventArgs) in c:\Adwords\examples\AdWords\CSharp\OAuth\Default.aspx.cs:line 130
InnerException:
The application was using these credentials in a previous application, seemingly without issue.
I've searched high and low for an answer to this, the exception says that the access token can't be refreshed but the ambiguous reasoning doesn't help a great deal.
Could someone offer any further insight?
Many thanks.
You should set OAuth2ClientId and OAuth2ClientSecret values in webconfig.
<add key="OAuth2ClientId" value="your client id"/>
<add key="OAuth2ClientSecret" value="your client secret"/>
You can get clientid and client secret code from
https://console.developers.google.com/project
Provide the OAuth2 client ID and secret. You can create one from
https://console.developers.google.com/project. See
https://developers.google.com/identity/protocols/OAuth2WebServer
for more details.
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'm using the Google Analytics API and I followed this SO question to set up the OAuth: https://stackoverflow.com/a/13013265/1299363
Here is my OAuth code:
public void SetupOAuth ()
{
var Cert = new X509Certificate2(
PrivateKeyPath,
"notasecret",
X509KeyStorageFlags.Exportable);
var Provider = new AssertionFlowClient(GoogleAuthenticationServer.Description, Cert)
{
ServiceAccountId = ServiceAccountUser,
Scope = ApiUrl + "analytics.readonly"
};
var Auth = new OAuth2Authenticator<AssertionFlowClient>(Provider, AssertionFlowClient.GetState);
Service = new AnalyticsService(Auth);
}
PrivateKeyPath is the path of the private key file provided by Google API Console. This works perfectly on my local machine, but when I push it up to our test server I get
System.Security.Cryptography.CryptographicException: An internal error occurred.
with the following stack trace (irrelevant parts removed):
System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr) +33
System.Security.Cryptography.X509Certificates.X509Utils._LoadCertFromFile(String fileName, IntPtr password, UInt32 dwFlags, Boolean persistKeySet, SafeCertContextHandle& pCertCtx) +0
System.Security.Cryptography.X509Certificates.X509Certificate.LoadCertificateFromFile(String fileName, Object password, X509KeyStorageFlags keyStorageFlags) +237
System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(String fileName, String password, X509KeyStorageFlags keyStorageFlags) +140
Metrics.APIs.GoogleAnalytics.SetupOAuth() in <removed>\Metrics\APIs\GoogleAnalytics.cs:36
Metrics.APIs.GoogleAnalytics..ctor(String PrivateKeyPath) in <removed>\Metrics\APIs\GoogleAnalytics.cs:31
So it appears as if it is having trouble loading the file. I've checked the PrivateKeyPath that is passed in and it is pointing to the correct location.
Any ideas? I don't know if this is an issue with the server, the file, the code or what.
One of things that comes to my mind is the identity of your app pool, make sure that the Load user profile is turned on otherwise the crypto subsystem does not work.
I'm loading my p12 file with
new X509Certificate2(
HostingEnvironment.MapPath(#"~/App_Data/GoogleAnalytics-privatekey.p12"), ....
I actually got a FileNotFoundException even though File.Exists(filename) returned true.
As #Wiktor Zychla said it's as simple as enabling Load User Profile
Here's an image of the setting that needs changing
Just right click on the app pool under 'Application Pools' in IIS and select 'Advanced Settings' and the setting you need is about halfway down.
Tip: I'd recommend commenting your code with this to prevent future time wasted since it's so obscure if you've never come across it before.
// If this gives FileNotFoundException see
// http://stackoverflow.com/questions/14263457/
Also try specifying X509KeyStorageFlags
var certificate = new X509Certificate2(KeyFilePath, KeyFilePassword,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable);
As mentioned above you need to configure IIS but as our case, some time you need to check the permission of the following folder:
C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys
If you set X509KeyStorageFlags parameter it will create a key file in this folder. In my case there was a difference in permission of this folder. Pool account was not added in the mentioned folder.
Nope, is "File.Exists(...)" also in advanced settings? I had 3 pools, all of them had true enabled for "Load User Profile". I'm thinking my problem might have something to do with dependencies and NuGet Packages as the code worked just fine as a Console App but gives me problem in MVC.
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 :)