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?
Related
I am trying to make use of nunit together with to do unit testing. However I am experiencing this error:
Moq.MockException: The following setups were not matched:
User m => m.encrptPassword(It.IsAny<String>(), It.IsAny<String>())
The following is the snippet of my code that is having issues:
private User _User;
Mock<User> mockUser;
[SetUp]
public void init()
{
mockUser = new Mock<User>();
_User = new User();
}
[Test]
[TestCase("1","admin","??????_????k0O???qr#??%szq??` ?j???????D>?????????Lvz??BP")]
public void encryptPasswordTest(string userID, string password, string output)
{
mockUser.Setup(m => m.encryptPassword(It.IsAny<string>(), It.IsAny<string>())).Returns(() => output);
string result = _User.encryptPassword(userID, password);
Assert.That(result.Equals(output));
mockUser.VerifyAll();
}
The following is the method that I am trying to mock
public virtual string encryptPassword(string userID, string password) {
string hashed = "";
HashAlgorithm sha256 = new SHA256CryptoServiceProvider();
string salted = userID + password;
byte[] result = sha256.ComputeHash(Encoding.ASCII.GetBytes(salted));
hashed = Encoding.ASCII.GetString(result);
return hashed;
}
You don't need any mocking in your case, you just have to check functions output:
[Test]
[TestCase("1","admin","??????_????k0O???qr#??%szq??` ?j???????D>?????????Lvz??BP")]
public void encryptPasswordTest(string userID, string password, string output)
{
string result = _User.encryptPassword(userID, password);
Assert.That(result.Equals(output));
}
You would need mocking if you'd need to validate your logic depending on output of another component. E.g. you have
public interface IEncrypter
{
string encryptPassword(string userID, string password);
}
and inject it to User class:
public User(IEncrypter encrypter) { this.encrypter = encrypter; }
Then you would mock it:
var mock = new Mock<IEncrypter>();
mock.Setup(m => m.encryptPassword(It.IsAny<string>(), It.IsAny<string>())).Returns(() => output);
var user = new User(mock.Object);
I need to read/write ActiveDirectory User object Terminal Services properties.
I tried this:
PrincipalContext context = new PrincipalContext(ContextType.Domain, "CA");
using (context)
{
UserPrincipal user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, "CA\\vlekovic");
if (user != null)
{
DirectoryEntry entry = (DirectoryEntry)user.GetUnderlyingObject();
entry.Properties["msTSProfilePath"].Value = "";
entry.Properties["msTSHomeDirectory"].Value = "";
entry.Properties["msTSHomeDrive"].Value = "";
entry.CommitChanges();
}
}
And I tried this:
PrincipalContext context = new PrincipalContext(ContextType.Domain, "CA");
using (context)
{
UserPrincipal user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, "CA\\vlekovic");
if (user != null)
{
DirectoryEntry entry = (DirectoryEntry)user.GetUnderlyingObject();
entry.InvokeSet("msTSProfilePath", "");
entry.InvokeSet("msTSHomeDirectory", "");
entry.InvokeSet("msTSHomeDrive", "");
entry.CommitChanges();
}
}
But nothing works.
I tried also with following property names:
TerminalServicesProfilePath
TerminalServicesHomeDirectory
TerminalServicesHomeDrive
But no luck. Any help would be appreciated!
Thanks,
Vojin
If you're on .NET 3.5 and up and using the System.DirectoryServices.AccountManagement (S.DS.AM) namespace, you can easily extend the existing UserPrincipal class to get at more advanced properties, like Manager etc.
Read all about it here:
Managing Directory Security Principals in the .NET Framework 3.5
MSDN docs on System.DirectoryServices.AccountManagement
Basically, you just define a derived class based on UserPrincipal, and then you define your additional properties you want:
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("Person")]
public class UserPrincipalEx : UserPrincipal
{
// Implement the constructor using the base class constructor.
public UserPrincipalEx(PrincipalContext context) : base(context)
{ }
// Implement the constructor with initialization parameters.
public UserPrincipalEx(PrincipalContext context,
string samAccountName,
string password,
bool enabled) : base(context, samAccountName, password, enabled)
{}
// Create the "TermSrvProfilePath" property.
[DirectoryProperty("msTSProfilePath")]
public string TermSrvProfilePath
{
get
{
if (ExtensionGet("msTSProfilePath").Length != 1)
return string.Empty;
return (string)ExtensionGet("msTSProfilePath")[0];
}
set { ExtensionSet("msTSProfilePath", value); }
}
}
Now, you can use the "extended" version of the UserPrincipalEx in your code:
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain))
{
// Search the directory for the new object.
UserPrincipalEx inetPerson = UserPrincipalEx.FindByIdentity(ctx, IdentityType.SamAccountName, "someuser");
// you can easily access the TermSrvProfilePath now
string path = inetPerson.TermSrvProfilePath;
}
The same way, you can also add the other properties you're looking for.
I did exactly the same way you told us.
I tried with an other fiel as
[DirectoryProperty("wWWHomePage")]
public string wWWHomePage
{
get
{
if (ExtensionGet("wWWHomePage").Length != 1)
return null;
return (string)ExtensionGet("wWWHomePage")[0];
}
set { this.ExtensionSet("wWWHomePage", value); }
}
But with the TermSrvProfilePath , it always return "empty" ...
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.");
}
}
}
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();
}
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);
}
}