I'm trying to do a one-way directory synchronization across domains (with no trust). Ideally I'd like to use an existing sync framework like Microsoft Sync Framework 2.1, but can't figure out how to set up the authentication correctly. I think I'll need to use NTLM pass-through authentication (described here), but initial tests are not showing success.
The code I'm working off of looks similar to this (based off the MSDN sample):
SafeTokenHandle safeTokenHandle;
bool returnValue = LogonUser("xfrtest", ".", "password", 8, 2, out safeTokenHandle);
using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle()))
{
using (WindowsImpersonationContext ctx = newId.Impersonate())
{
sourceProvider = new FileSyncProvider(
sourceReplicaRootPath, filter, options);
destinationProvider = new FileSyncProvider(
destinationReplicaRootPath, filter, options);
SyncOrchestrator agent = new SyncOrchestrator();
agent.LocalProvider = sourceProvider;
agent.RemoteProvider = destinationProvider;
agent.Direction = SyncDirectionOrder.Upload;
sourceProvider.DetectChanges();
destinationProvider.DetectChanges();
var results = agent.Synchronize();
}
}
I have the "xfrtest" account set up locally on both machines with the same password. The logon works for the source system (which I'm also running the code on) but fails on the target.
So the questions are...first, is it possible to do this using Sync Framework? If so, am I approaching it wrong with pass-through? If not, any suggestions for good alternatives?
As it turns out, this code actually DOES work. My issue was that my non-production domains I was testing in had trust, which causes Windows to not fall back to NTLM pass-through authentication. When I set up domains with no trust or non-domain machines, this works.
Related
I'm kind of new to the whole WCF and SOAP topic so please be kind.
I'm using a generated SOAP Client with .net6. In another project we successfully worked with the same Web Service using the old .net Framework 2.0 Web References and the same credentials.
Strange enough everything seemed to work fine at first. Until I realized, that it does not use the given credentials to authenticate. Instead it authenticates with my own domain user.
I also tried to get it to work with explicitly setting the binding with a BasicHttpBinding but I only could get the same broken logic to work or I got various authentication/protocol/security errors.
So it seems the authentication is basically working. It just doesn't use the provided credentials. So my question is: How can I configure it to work with the provided identity?
I also found out that it might have anything to do with a cached Windows token. But how can I get rid of it. How to prevent caching in the first place?
EDIT:
Specified the variable types explicitly.
string url = "http://someServer/AdministrationService.asmx";
AdministrationServiceSoapClient client = new AdministrationServiceSoapClient(
AdministrationServiceSoapClient.EndpointConfiguration.AdministrationServiceSoap,
url);
WindowsClientCredential credential = client.ClientCredentials.Windows;
credential.ClientCredential.UserName = "username";
credential.ClientCredential.Password = "password";
credential.ClientCredential.Domain = "DOMAIN";
GetServerInfoRequest getServerInfoRequest = new GetServerInfoRequest
{
// some stuff set here
};
GetServerInfoRequest getServerInfoReply = await client.GetServerInfoAsync(getServerInfoRequest);
As far as I know, BasicHttpBinding has security disabled by default, but can be added setting the BasicHttpSecurityMode to a value other than None in the constructor. It can be configured according to the instructions in BasicHttpBinding and BasicHttpBinding Constructors.
By default, setting up client credentials involves two steps: determining the type of client credential required by the service and specifying an actual client credential, as described in this document.
After waiting a day it is working. It seems that the cached credentials became invalid somehow.
Strange enough the simple service creation from above is not working anymore. Instead I have to use the following.
var client = new AdministrationServiceSoapClient(
new BasicHttpBinding()
{
Security = new BasicHttpSecurity()
{
Mode = BasicHttpSecurityMode.TransportCredentialOnly,
Message = new BasicHttpMessageSecurity()
{
ClientCredentialType = BasicHttpMessageCredentialType.UserName,
},
Transport = new HttpTransportSecurity()
{
ClientCredentialType = HttpClientCredentialType.Windows,
ProxyCredentialType = HttpProxyCredentialType.Windows,
}
},
},
new EndpointAddress(url));
I have a .NET 5.0 WinForms app that uses WebView2 with the evergreen runtime. I create my own environment with allowSingleSIgnOnUsingOSPrimaryAccount set to true (see snippet below). This results in the user opening up the app viewing our AzureAD fronted web app and authenticating against our app reg without the need to type in a user/pass or go through MFA.
var _cacheFolderPath = Path.Combine(Application.UserAppDataPath, "Myappname.exe.WebView2");
CoreWebView2EnvironmentOptions webViewEnvironmentOptions = new CoreWebView2EnvironmentOptions(allowSingleSignOnUsingOSPrimaryAccount: _config.UseWindowsAuth);
var webView2Environment = CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: _cacheFolderPath, options: webViewEnvironmentOptions).Result;
webView.EnsureCoreWebView2Async(webView2Environment);
On most machines, this works as expected, but there are a few machines where users are prompted for password. So a user that seamlessly logs in to our web app when logged into Windows on their primary machine may go to one of these particular machines and get prompted for an email/pass and MFA. I'm not seeing errors, event logs, etc....it just seems as though setting this value to true in code is simply being ignored or overridden.
I've tried to look for documentation related to Group Policy settings possibly being the cause, but there is not a lot I found regarding this for WebView2. Is there anything that is/can be set explicitly through GP, or some other mechanism that may be having some effect WebView2's behavior regarding allowSingleSignOnUsingOSPrimaryAccount?
Maybe try to go with something like this:
var options = new CoreWebView2EnvironmentOptions
{
AllowSingleSignOnUsingOSPrimaryAccount = true,
AdditionalBrowserArguments = "--auth-server-whitelist=_"
};
var userdatafolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Incognito", new Random().Next().ToString());
var environment = await CoreWebView2Environment.CreateAsync(null, userdatafolder, options: options);
Debug.WriteLine("InitializeAsync");
await WebView21.EnsureCoreWebView2Async(environment);
Debug.WriteLine($"WebView2 Runtime version: {WebView21.CoreWebView2.Environment.BrowserVersionString}");
New question
Update: since this started with some general challenges and has since zeroed in on a more specific issue I've re-posted as a new question here.
I have been following Microsoft's advice for sharing an authentication cookie issued by an ASP.NET web app with a separate dotnet core web app running on the same domain. Unfortunately the dotnet core app is not unprotecting the cookie as expected and I'm struggling to diagnose why.
I'll try to simplify what I've done. Before I do I should point out that both apps will run under the same path - let's call it mydomain.com/path - so the auth cookie will be scoped to that path. There's a lot of additional complexity because I'm actually trying to wire this into an old OIDC library, but I think the main issue I'm having is on the other side where I have a fairly lightweight dotnet core app trying to use the same session.
First, in my original .NET app (it's 4.7.2) I'm creating a new data protector using the Microsoft.Owin.Security.Interop library:
var appName = "<my-app-name>";
var encryptionSettings = new AuthenticatedEncryptionSettings()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
};
var interopProvider = DataProtectionProvider.Create(
new DirectoryInfo(keyRingSharePath),
builder =>
{
builder.SetApplicationName(appName);
builder.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20));
builder.UseCryptographicAlgorithms(encryptionSettings);
if (!generateNewKey)
{
builder.DisableAutomaticKeyGeneration();
}
});
return new DataProtectorShim(
interopProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
appName,
"v2"));
Note that <my-app-name> is also the name of the cookie, as set in the CookieAuthenticationOptions.
keyRingSharePath is for now just a local path on my PC. The first time I run this I have generateNewKey set to true to ensure a new key is generated at this path. Thereafter I leave this false to ensure that key is re-used.
I also assign the ticket data format using this data protector as per the docs: new TicketDataFormat(dataProtector).
This works as expected in that authentication still works and I can even verify the data protection by using an instance of the TicketDataFormat created above and calling its Unprotect method with the auth cookie value and getting a ClaimsIdentity back.
Next I've created a simple dotnet core app which runs on the same domain as the above app. In the Startup I've added this:
var primaryAuthenticationType = "<my-app-name>";
var cookieName = primaryAuthenticationType;
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keyRingSharePath))
.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20))
.DisableAutomaticKeyGeneration()
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
})
.SetApplicationName(primaryAuthenticationType);
services.ConfigureApplicationCookie(options => {
options.Cookie.Name = cookieName;
options.Cookie.Path = "/path";
});
Obviously keyRingSharePath holds the same value as in the ASP.NET app. I also have the following in the ConfigureServices method in Startup:
app.UseAuthentication();
app.UseAuthorization();
Having signed in using the ASP.NET app I then switch to my dotnet core app. But unfortunately when debugging any controller with a route under /path I find that User.Identity.IsAuthenticated is false.
I've also tried unprotecting the cookie manually like this, using an injected instance of IDataProtectionProvider:
var protector = protectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
"<my-app-name>",
"v2");
var ticketDataFormat = new TicketDataFormat(dataProtector);
var ticket = ticketDataFormat.Unprotect("<auth-cookie-value>");
return ticket?.Principal;
However, ticket is assigned null and I can't find any way to debug why it won't unprotect the cookie value. As far as I can tell this should use the same logic that my ASP.NET app used when I confirmed that I could unprotect that cookie.
Any guidance would be much appreciated.
UPDATE 1
I've been playing around a bit more by trying to deconstruct the code that unprotects the cookie. I've added the following code to a controller on my dotnet core app:
var formatVersion = 3;
var protector = _dataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "<my-app-name>", "v2");
var cookieValue = Request.Cookies["<my-app-name>"];
var cookieValueDecoded = Base64UrlTextEncoder.Decode(cookieValue);
var unprotectedBytes = protector.Unprotect(cookieValueDecoded);
using (MemoryStream memoryStream = new MemoryStream(unprotectedBytes))
{
using (GZipStream gzipStream = new GZipStream((Stream)memoryStream, CompressionMode.Decompress))
{
using (BinaryReader reader = new BinaryReader((Stream) gzipStream))
{
if (reader.ReadInt32() != formatVersion) return (AuthenticationTicket) null;
string authenticationType = reader.ReadString();
string str1 = ReadWithDefault(reader, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
string roleType = ReadWithDefault(reader, "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
int length = reader.ReadInt32();
Claim[] claimArray = new Claim[length];
for (int index = 0; index != length; ++index)
{
string type = ReadWithDefault(reader, str1);
string str2 = reader.ReadString();
string valueType = ReadWithDefault(reader, "http://www.w3.org/2001/XMLSchema#string");
string str3 = ReadWithDefault(reader, "LOCAL AUTHORITY");
string originalIssuer = ReadWithDefault(reader, str3);
claimArray[index] = new Claim(type, str2, valueType, str3, originalIssuer);
}
ClaimsIdentity identity = new ClaimsIdentity((IEnumerable<Claim>)claimArray, authenticationType, str1, roleType);
}
}
}
Most of this code comes from Microsoft.Owin.Security.DataHandler.Serializer.TicketSerializer in Microsoft.Owin.Security, Version=3.0.1. It is therefore essentially a reversal of the protection logic that is used in the originating ASP.NET app, and it works fine. It ends with a ClaimsIdentity that matches the account which authenticated on the other app. This tells me that the cryptography config and keys are matched between the apps.
So there must be some other difference between the built-in code that unprotects the authentication cookie on both sides. But I'm unclear about how to diagnose where the difference is. My assumption is that I've missed something on the dotnet core side which makes the cookie authentication interoperable.
UPDATE 2
Having dug around a bit more I think this comes down to the data serializer format version. In my dotnet core app if I dig into the TicketDataFormat I see it uses TicketSerializer.Default which is an implementation of IDataSerializer<AuthenticationTicket> that has a hard-coded FormatVersion of 5. There's also a comment at the top of TicketSerializer saying:
This MUST be kept in sync with Microsoft.Owin.Security.Interop.AspNetTicketSerializer
However you can see in my UPDATE 1 above that when I ripped out some of the serialization logic from the ASP.NET web app, it is working with a format version of 3. Note that this app is using the version of TicketDataFormat that comes with Microsoft.Owin.Security, Version=3.0.1.0 and that assembly has a TicketSerializer with hard-coded FormatVersion of 3.
So, how can I keep ensure these serializers are compatible on both sides?
UPDATE 3
Realised I'm a total tool and was missing a key part of the Microsoft docs. Above I state this:
I also assign the ticket data format using this data protector as per the docs: new TicketDataFormat(dataProtector).
Well, actually I should have been using the AspNetTicketDataFormat type provided by the Microsoft.Owin.Security.Interop library. Having corrected this I can now obtain a claim principal in my dotnet core app using the following:
var dataProtector = _dataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "<my-app-name>", "v2");
var ticketDataFormat = new TicketDataFormat(dataProtector);
var ticket = ticketDataFormat.Unprotect(cookieValue, "");
Here I can see ticket.Principal.Identity populated with my identity from the cookie.
However, I still can't get my app in an authenticated state. I'm clearly still not wiring up the cookie authentication middleware correctly. My Startup still just looks like the second code block in my original post. Feels like the final hurdle if anyone can help.
Eventually by the time I reached Update 3 I had been working on this long enough that the session had expired but I was still using that session to test. So the reason my cookie was being rejected was due to session expiry, not some coding issue. I discovered this once I added trace-level logging to the app and there was the answer, staring at me from the console! Will leave this here in case the process described in the post benefits anyone else.
please take a few minutes and read my question completely. here is my problem :
I want to connect to LDAP server by C# for a web application, means clients connecting to the asp.net server.
*- The ldap server and application server are not the same.
**- They are not in a same domain.
I have been trying 4 different ways and could not solve the problem by none of them.
1-
var credentials = new NetworkCredential(username, password);
var serverId = new LdapDirectoryIdentifier("domain.net");
var conn = new LdapConnection(serverId, credentials);
conn.Bind();
2-
System.DirectoryServices.DirectoryEntry entry = new System.DirectoryServices.DirectoryEntry("LDAP://domain.net/DC=domain,DC=net");
entry.Username = "username";
entry.Password = "password";
System.DirectoryServices.DirectorySearcher searcher = new System.DirectoryServices.DirectorySearcher(entry);
searcher.Filter = "(&(objectClass=user))";
var results = searcher.FindAll();
the problem with these 2 ways is that the user must have an access to the server for login and we know that there is only admin of the system who has the permission.
3-
PrincipalContext pc = new PrincipalContext(ContextType.Domain, "domain.net");
var ret = pc.ValidateCredentials(model.UserName, model.Password);
the problem is the server must be in the domain of ldap server. we have this limitation !!
4-
https://auth0.com/blog/using-ldap-with-c-sharp/
public bool validateUser(string username, string password)
{
var sha1 = new SHA1Managed();
var digest = Convert.ToBase64String(sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)));
var request = new CompareRequest(string.Format("uid={0},ou=users,dc=example,dc=com", username),
"userPassword", "{SHA}" + digest);
var response = (CompareResponse)connection.SendRequest(request);
return response.ResultCode == ResultCode.CompareTrue;
}
this code does not return any thing. it will be very helpful if there is a query to compare usernames and passwords. this code seems to use this way but there are different types of hash algorithms. I tried to use SHA1 and MD5, and userPassword , unicodePwd attribute. but the return is empty all the time.
is it the best solution to put both servers in a same domain? any other solution , Thank you so much.
Method 1 is the simplest way. If that doesn't work, nothing will. So you will have to change your configuration to make it work.
When you say:
the user must have an access to the server for login and we know that there is only admin of the system who has the permission.
What do you mean? Are you explicitly denying login rights to the domain controllers? Are there network issues between the computer this is running on and the domain controller (can you hit port 389 on the domain controller(s))?
Update: PrincipalContext.ValidateCredentials just does an LDAP bind in behind - it uses LdapConnection. You can see the source code here. The probable reason why ValidateCredentials is working on a domain machine and not otherwise is because it uses Kerberos authentication by default, which will only work from a domain computer.
The same is true with LdapConnection. So try setting the authentication mode. For example, try Basic
var credentials = new NetworkCredential(username, password);
var serverId = new LdapDirectoryIdentifier("domain.net");
var conn = new LdapConnection(serverId, credentials, AuthType.Basic);
conn.Bind();
Or look at the available values and try what works.
Currently I'm trying to figure out, how to add a VPN profile and connect to it from my universal app. I can connect to existing VPN connections with the Windows.Networking.Vpn namespace. I can also add a profile, but can not find a way to set all the required information (PSK for example). There is no documentation about this namespace in the MS docs. I also saw that there are two different profile namespaces available: VpnNativeProfile and VpnPlugInProfile. What is the difference between them? Currently I'm not at home, so I can't provide my current code, but it would be very helpful if someone can give me some hints. Is there a documentation available somewhere else?
Edit 1//
Here is my sample Code
Creating a profile
VpnManagementAgent mgr = new VpnManagementAgent();
VpnNativeProfile profile = new VpnNativeProfile()
{
AlwaysOn = false,
NativeProtocolType = VpnNativeProtocolType.L2tp,
ProfileName = "MyConnection",
RememberCredentials = true,
RequireVpnClientAppUI = true,
RoutingPolicyType = VpnRoutingPolicyType.SplitRouting,
TunnelAuthenticationMethod = VpnAuthenticationMethod.PresharedKey,
UserAuthenticationMethod = VpnAuthenticationMethod.Mschapv2,
};
profile.Servers.Add("vpn.example.com");
VpnManagementErrorStatus profileStatus = await mgr.AddProfileFromObjectAsync(profile);
Connecting to the VPN
PasswordCredential credentials = new PasswordCredential
{
UserName = "username",
Password = "password",
};
VpnManagementErrorStatus connectStatus = await mgr.ConnectProfileWithPasswordCredentialAsync(profile, credentials);
This works, but i don't know where or how to set the PSK.
VPN Native Profile : This refers to a Windows Inbox / Built-In VPN profile and can be used for L2TP, PPTP or IKEv2 based VPN
VPN Plugin Profile : Refers to a Windows 10 UWP based VPN Plugin. This is a VPN app written using the Windows.networking.VPN namespace.
I also took a peek at the code and can see that there seems to be a very obvious miss where there isnt really a way to set the PSK via the code. The only real workaround would be to set it in the Settings UI for now.
I will go ahead and report to the VPN team for Windows about this being missing.
Documentation Link : https://learn.microsoft.com/en-us/uwp/api/windows.networking.vpn