Validate user credentials against domain controller in .net - c#

in an .NET application, I'm trying to authenticate users by username and password a against windows users, local ones as well as domain users. I already tried this solution . My code to get the PrincipalContext looks the following:
protected static PrincipalContext TryCreatePrincipalContext(String domain)
{
var computerDomain = TryGetComputerDomain();
if (String.IsNullOrEmpty(domain) && String.IsNullOrEmpty(computerDomain))
return new PrincipalContext(ContextType.Machine);
else if (String.IsNullOrEmpty(domain))
return new PrincipalContext(ContextType.Domain, computerDomain);
else
return new PrincipalContext(ContextType.Domain, domain);
}
protected static String TryGetComputerDomain()
{
try
{
var domain = Domain.GetComputerDomain();
return domain.Name;
} catch
{
return null;
}
}
That works fine for local windows users users and for remote users in an ActiveDirectory. But if I try to run the authentication on a machine, that is joined to a non-ActiveDirectory Domain Master, eg. a Samba Server I get the following Exception:
System.DirectoryServices.AccountManagement.PrincipalServerDownException: Mit dem Server konnte keine Verbindung hergestellt werden. --->
System.DirectoryServices.Protocols.LdapException: Der LDAP-Server ist nicht verfügbar.
bei System.DirectoryServices.Protocols.LdapConnection.Connect()
bei System.DirectoryServices.Protocols.LdapConnection.SendRequestHelper(DirectoryRequest request, Int32& messageID)
bei System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request, TimeSpan requestTimeout)
bei System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request)
bei System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
--- Ende der internen Ausnahmestapelüberwachung ---
bei System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
bei System.DirectoryServices.AccountManagement.PrincipalContext.DoServerVerifyAndPropRetrieval()
bei System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name, String container, ContextOptions options, String userName, String password)
bei System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name)
bei DomainAuthTest.DomainAuthenticator.TryCreatePrincipalContext(String domain)
bei DomainAuthTest.DomainAuthenticator.Authenticate(String domainUser, String password)
bei DomainAuthTest.Program.Main(String[] args)
So it seems that the PrincipalContext tries to use LDAP in case of ContextType.Domain. If I try to use ContextType.Machine I have cannot use the workgroup/domain-name as PrincipalContext tries to connect directly to the machine. That fails if there is already a connection to that machine with that windows from the same machine.
So my question is:
How to authenticate a user with the credentials domain, username and password against a domain master, which is not necessarily based on an ActiveDirectory?
Are there managed APIs to accomplish the above described task?
If there are no managed foundation-classes, what is the right direction to do that with?
Thank you for your replies.

For the sake of completeness, here my solution which seems to do exactly what I want:
public class WinApiDomainAuthenticator
{
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out IntPtr phToken);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);
public static IPrincipal Authenticate(String domainUser, String password)
{
var userToken = IntPtr.Zero;
var creds = new DomainAuthCredentials(domainUser, password);
if (! LogonUser(creds.Username,
creds.Domain,
creds.Password,
(int)LogonType.LOGON32_LOGON_BATCH,
(int)LogonProvider.LOGON32_PROVIDER_DEFAULT, out userToken))
{
var error = new Win32Exception(Marshal.GetLastWin32Error());
throw new SecurityException("Error while authenticating user", error);
}
var identity = new WindowsIdentity(userToken);
if (userToken != IntPtr.Zero)
CloseHandle(userToken);
return ConvertWindowsIdentityToGenericPrincipal(identity);
}
protected static IPrincipal ConvertWindowsIdentityToGenericPrincipal(WindowsIdentity windowsIdentity)
{
if (windowsIdentity == null)
return null;
// Identity in format DOMAIN\Username
var identity = new GenericIdentity(windowsIdentity.Name);
var groupNames = new string[0];
if (windowsIdentity.Groups != null)
{
// Array of Group-Names in format DOMAIN\Group
groupNames = windowsIdentity.Groups
.Select(gId => gId.Translate(typeof(NTAccount)))
.Select(gNt => gNt.ToString())
.ToArray();
}
var genericPrincipal = new GenericPrincipal(identity, groupNames);
return genericPrincipal;
}
protected class DomainAuthCredentials
{
public DomainAuthCredentials(String domainUser, String password)
{
Username = domainUser;
Password = password;
Domain = ".";
if (!domainUser.Contains(#"\"))
return;
var tokens = domainUser.Split(new char[] { '\\' }, 2);
Domain = tokens[0];
Username = tokens[1];
}
public DomainAuthCredentials()
{
Domain = String.Empty;
}
#region Properties
public String Domain { get; set; }
public String Username { get; set; }
public String Password { get; set; }
#endregion
}
}
The LogonType and LogonProvider enums reflect the definitions in "Winbase.h". I settled with LogonType.LOGON32_LOGON_BATCH instead of LogonType.LOGON32_LOGON_NETWORK because samba 3.4.X seems to have trouble with this type.

Here is one that I just did for an app I'm working on myself - requires Framework v3.5 or greater....
public static bool Authenticate(string user, string password)
{
// Split the user name in case a domain name was specified as DOMAIN\USER
string[] NamesArray = user.Split(new char[] { '\\' }, 2);
// Default vars for names & principal context type
string DomainName = string.Empty;
string UserName = string.Empty;
ContextType TypeValue = ContextType.Domain;
// Domain name was supplied
if (NamesArray.Length > 1)
{
DomainName = NamesArray[0];
UserName = NamesArray[1];
}
else
{
// Pull domain name from environment
DomainName = Environment.UserDomainName;
UserName = user;
// Check this against the machine name to pick up on a workgroup
if (string.Compare(DomainName, System.Environment.MachineName, StringComparison.InvariantCultureIgnoreCase) == 0)
{
// Use the domain name as machine name (local user)
TypeValue = ContextType.Machine;
}
}
// Create the temp context
using (PrincipalContext ContextObject = new PrincipalContext(TypeValue, DomainName))
{
// Validate the credentials
return ContextObject.ValidateCredentials(UserName, password);
}
}

Related

Passing credentials in IP address

I am trying to access media directly from a network share on our local network.
The problem is that I need to pass windows credentials with the url.
I have tried impersonation with logon type 9, and I have tried passing the credentials in the url like this:
#"\\username:password#ip\share_name\path\filename.mkv"
I am trying to access the media in a windows media player in a winform project, and the player just loads something and goes to a ready state.
When typing the address in explorer it asks for credentials, which is what I expected, but how do I do that in my case? I feel like I have tried everything..
token = IntPtr.Zero;
LogonUser("Username", "NAS-IP", "Password",
9, 0, ref token);
person = new WindowsIdentity(token).Impersonate();
axWindowsMediaPlayer1.URL = #"\\ip\camera_share\axis-ACCC8E7B9050\20170712\08\20170712_085720_39AA_ACCC8E7B9050\20170712_08\20170712_085720_0092.mkv";
EDIT
For some reason it works if I use the machinename/servername as address in the media player url. This is just not that efficient if the client only knows the ip of the server not the name.
token = IntPtr.Zero;
LogonUser("username", "serverip", "password",
9, 0, ref token);
person = new WindowsIdentity(token).Impersonate();
axWindowsMediaPlayer1.URL = #"\\servername\camera_share\axis-ACCC8E7B9050\20170719\10\20170719_100732_8084_ACCC8E7B9050\20170719_10\20170719_100732_E5A7.mkv";
Any idea on how to work around that?
You need to use NetworkCredential to log into remote locations.
using System.IO;
using System.Net;
NetworkCredential theNetworkCredential = new
NetworkCredential(#"domain\username", "password");
CredentialCache theNetCache = new CredentialCache();
theNetCache.Add(new Uri(#"\\computer"), "Basic", theNetworkCredential);
string[] theFolders = Directory.GetDirectories(#"\\computer\share");
After some extensive research I found a very viable answer in the thread: How to provide user name and password when connecting to a network share
Luke Quinane made the following code:
public class NetworkConnection : IDisposable
{
string _networkName;
public NetworkConnection(string networkName,
NetworkCredential credentials)
{
_networkName = networkName;
var netResource = new NetResource()
{
Scope = ResourceScope.GlobalNetwork,
ResourceType = ResourceType.Disk,
DisplayType = ResourceDisplaytype.Share,
RemoteName = networkName
};
var userName = string.IsNullOrEmpty(credentials.Domain)
? credentials.UserName
: string.Format(#"{0}\{1}", credentials.Domain, credentials.UserName);
var result = WNetAddConnection2(
netResource,
credentials.Password,
userName,
0);
if (result != 0)
{
throw new Win32Exception(result, "Error connecting to remote share");
}
}
~NetworkConnection()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
WNetCancelConnection2(_networkName, 0, true);
}
[DllImport("mpr.dll")]
private static extern int WNetAddConnection2(NetResource netResource,
string password, string username, int flags);
[DllImport("mpr.dll")]
private static extern int WNetCancelConnection2(string name, int flags,
bool force);
}
[StructLayout(LayoutKind.Sequential)]
public class NetResource
{
public ResourceScope Scope;
public ResourceType ResourceType;
public ResourceDisplaytype DisplayType;
public int Usage;
public string LocalName;
public string RemoteName;
public string Comment;
public string Provider;
}
public enum ResourceScope : int
{
Connected = 1,
GlobalNetwork,
Remembered,
Recent,
Context
};
public enum ResourceType : int
{
Any = 0,
Disk = 1,
Print = 2,
Reserved = 8,
}
public enum ResourceDisplaytype : int
{
Generic = 0x0,
Domain = 0x01,
Server = 0x02,
Share = 0x03,
File = 0x04,
Group = 0x05,
Network = 0x06,
Root = 0x07,
Shareadmin = 0x08,
Directory = 0x09,
Tree = 0x0a,
Ndscontainer = 0x0b
}
Which I could use like this:
using (new NetworkConnection(#"\\IP", new NetworkCredential("Username", "Password", System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName)))
{
axWindowsMediaPlayer1.URL = #"\\IP\camera_share\axis-ACCC8E7B9050\20170719\10\20170719_100732_8084_ACCC8E7B9050\20170719_10\20170719_100732_E5A7.mkv";
}
Thanks Luke!

How to get client domain name from a request

I have wcf-service host at kkk.com. When someone(other domain) request to my service I use this code to get client's domain name.
public static string GetClientDomian(HttpContext context)
{
string clientDomain = string.Empty;
clientDomain = context.Request.Url.Host;
return clientDomain;
}
But it's always return kkk.com not return the client's domain name. How to solve this? Thanks.
OK. This code is work. Change context.Request.Url.Host to context.Request.UrlReferrer.Host
Like this
public static string GetClientDomian(HttpContext context)
{
string clientDomain = string.Empty;
clientDomain = context.Request.UrlReferrer.Host;
return clientDomain;
}

Retrieving a user principal object using down-level user name

I'm having problems retrieving the User Principal object from AD as follows:
public static UserPrincipal GetUserPrincipalByUserName(string userName, IdentityType identityType, string adUsername, string adPassword, string adDomain)
{
UserPrincipal result;
try
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, adDomain, adUsername, adPassword))
{
result = UserPrincipal.FindByIdentity(pc, identityType, userName);
}
}
catch
{
result = null;
}
return result;
}
All pretty normal right? However, in my web application, I'm pulling out the username from User.Identity.Name, which gives me the username in the down-level format (domain\username), not my UPN (username#domain.com). My unit tests (1 and 2) pass on the UPN or SAM IdentityTypes, but not on the down-level name provided (3), nor the unqualified username (4), using IdentityType.Name:
[TestClass]
public class ActiveDirectoryTests
{
public const string Username = "jdoe";
public const string DownLevelUsername = "DOMAIN\\jdoe";
public const string Upn = "jdoe#domain.com";
public const string AdUsername = "username";
public const string AdPassword = "password";
public const string AdDomain = "domain";
[TestMethod]
public void SearchByUpn()
{
Assert.IsNotNull(ActiveDirectory.SafeGetUserPrincipalByUserName(Upn, IdentityType.UserPrincipalName, AdUsername, AdPassword, AdDomain));
}
[TestMethod]
public void SearchBySamUsername()
{
Assert.IsNotNull(ActiveDirectory.SafeGetUserPrincipalByUserName(Username, IdentityType.SamAccountName, AdUsername, AdPassword, AdDomain));
}
[TestMethod]
public void SearchByDownLevelUsername()
{
Assert.IsNotNull(ActiveDirectory.SafeGetUserPrincipalByUserName(DownLevelUsername, IdentityType.Name, AdUsername, AdPassword, AdDomain));
}
[TestMethod]
public void SearchByUnqualifiedUsername()
{
Assert.IsNotNull(ActiveDirectory.SafeGetUserPrincipalByUserName(Username, IdentityType.Name, AdUsername, AdPassword, AdDomain));
}
}
Can I do this task without just doing some arbitrary string parsing on the down-level name that I get from User.Identity.Name? Can/should I just dig the SID out of the user object and use that instead?
I fixed my own problem just by using the SID, but for info:
The down-level domain name doesn't directly map to a UPN (missing info on the domain suffix), so basically you can't do text transforms between the two
The User.Identity.Name is still a mystery - see my other question here: What does System.DirectoryServices.AccountManagement.IdentityType.Name specify?

How can I authenticate against Active Directory in Nancy?

It's an outdated article, but http://msdn.microsoft.com/en-us/library/ff650308.aspx#paght000026_step3 illustrates what I want to do. I've chosen Nancy as my web framework because of it's simplicity and low-ceremony approach. So, I need a way to authenticate against Active Directory using Nancy.
In ASP.NET, it looks like you can just switch between a db-based membership provider and Active Directory just by some settings in your web.config file. I don't need that specifically, but the ability to switch between dev and production would be amazing.
How can this be done?
Really the solution is much simpler than it may seem. Just think of Active Directory as a repository for your users (just like a database). All you need to do is query AD to verify that the username and password entered are valid. SO, just use Nancy's Forms Validation and handle the connetion to AD in your implementation of IUserMapper. Here's what I came up with for my user mapper:
public class ActiveDirectoryUserMapper : IUserMapper, IUserLoginManager
{
static readonly Dictionary<Guid, long> LoggedInUserIds = new Dictionary<Guid, long>();
readonly IAdminUserValidator _adminUserValidator;
readonly IAdminUserFetcher _adminUserFetcher;
readonly ISessionContainer _sessionContainer;
public ActiveDirectoryUserMapper(IAdminUserValidator adminUserValidator, IAdminUserFetcher adminUserFetcher, ISessionContainer sessionContainer)
{
_adminUserValidator = adminUserValidator;
_adminUserFetcher = adminUserFetcher;
_sessionContainer = sessionContainer;
}
public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
{
_sessionContainer.OpenSession();
var adminUserId = LoggedInUserIds.First(x => x.Key == identifier).Value;
var adminUser = _adminUserFetcher.GetAdminUser(adminUserId);
return new ApiUserIdentity(adminUser);
}
public Guid Login(string username, string clearTextPassword, string domain)
{
var adminUser = _adminUserValidator.ValidateAndReturnAdminUser(username, clearTextPassword, domain);
var identifier = Guid.NewGuid();
LoggedInUserIds.Add(identifier, adminUser.Id);
return identifier;
}
}
I'm keeping a record in my database to handle roles, so this class handles verifying with AD and fetching the user from the database:
public class AdminUserValidator : IAdminUserValidator
{
readonly IActiveDirectoryUserValidator _activeDirectoryUserValidator;
readonly IAdminUserFetcher _adminUserFetcher;
public AdminUserValidator(IAdminUserFetcher adminUserFetcher,
IActiveDirectoryUserValidator activeDirectoryUserValidator)
{
_adminUserFetcher = adminUserFetcher;
_activeDirectoryUserValidator = activeDirectoryUserValidator;
}
#region IAdminUserValidator Members
public AdminUser ValidateAndReturnAdminUser(string username, string clearTextPassword, string domain)
{
_activeDirectoryUserValidator.Validate(username, clearTextPassword, domain);
return _adminUserFetcher.GetAdminUser(1);
}
#endregion
}
And this class actually verifies that the username/password combination exist in Active Directory:
public class ActiveDirectoryUserValidator : IActiveDirectoryUserValidator
{
public void Validate(string username, string clearTextPassword, string domain)
{
using (var principalContext = new PrincipalContext(ContextType.Domain, domain))
{
// validate the credentials
bool isValid = principalContext.ValidateCredentials(username, clearTextPassword);
if (!isValid)
throw new Exception("Invalid username or password.");
}
}
}

Membership.GetUser() // GetUser(userName) returns a null user

I'm trying to get a System.Web.Security.MembershipUser user so I can check it's lastPasswordChangedDate property, only problem is it's returning null.
I've had a look around and most of the similar problems seem to be because the user is not authenticated when trying to create the object.
In my case the user is authenticated directly before calling the method, albeit in another class.
Any pointers?
PSEUDO-CODE FOLLOWS :
FrontEnd ---->
Login.cs
string userName = txtUser.Text; string password = txtPW.Text;
wsAuth = new wsAuth();
wsAuthentication.Authenticate(userName,password);
Auth service ---->
public bool Authenticate(string userName, string password)
{
SecurityProviderSettings settings = SecurityProviderSettings.GetFromConfiguration("SecurityProviders");
ProviderSetting providerSetting = settings.Providers[providerName];
object provider = Activator.CreateInstance(providerType);
return ((IAuthenticationProvider)provider).Authenticate(userName, password, providerSetting.CustomInfo, out failReason, out token);
}
Specific Provider ---->
public bool Authenticate(string userName, string password, out string failReason .....)
{
SqlMembershipProvider provider = new SqlMembershipProvider();
#region settings
//blablabla
#endregion
provider.initialize(/*input vars*/);
bool authenticated = provider.ValidateUser(userName,password);
if(authenticated)
{
pwValidator val = new pwValidator();
if(!val.IsInLifetime(userName,password))
{
failReason = "password expired";
}
}
}
PasswordValidator ---->
public bool IsInLifetime(string userName, string password)
{
MembershipUser user = Membership.GetUser(userName);
MembershipUser user2 = Membership.GetUser();
#bla bla bla
}
Try something like this.
Create a method to get the user
HttpContext httpContext = HttpContext.Current;
if (httpContext != null && httpContext.User != null && httpContext.User.Identity.IsAuthenticated)
{
return Membership.GetUser();
}

Categories