Well I am using DirectoryEntry and LdapConnection to reset password in a scenario where we have password minimum age and history policy enforced. When someone forgets their password, you want them to be able to reset their password to something which doesn't violate password history. As an alternative solution, it would be possible to use "SetPassword" and reset the password to a generated value and then force the user to change it on their next login. This is not possible in our scenario. Hence, I was following this blog post in technet and trying out LDap extended controls to reset password by honoring password history. In brief, it's just changing to the same password again and again without complaints. My code is as follows:
private static void PasswordChanger(DirectoryConnection ldapCon,
string distinguishedName,
string passwordToSet = null)
{
// the 'unicodePWD' attribute is used to handle pwd handling requests
// modification control for the replace operation
var damReplace = new DirectoryAttributeModification
{
Name = "unicodePwd"
};
// value to be send with the request
damReplace.Add(Encoding.Unicode.GetBytes(String.Format("\"{0}\"", passwordToSet)));
// this is a replace operation
damReplace.Operation = DirectoryAttributeOperation.Replace;
// combine modification controls
var damList = new DirectoryAttributeModification[]
{
damReplace
};
// init modify request
var modifyRequest = new ModifyRequest(distinguishedName, damList);
// the actual extended control OID
const string ldapServerPolicyHintsOid = "1.2.840.113556.1.4.2239";
// build value utilizing berconverter
var value = BerConverter.Encode("{i}", new object[] { 0x1 });
// init exetnded control. The variable name represts the actual extended control name.
var LDAP_SERVER_POLICY_HINTS_OID = new DirectoryControl(ldapServerPolicyHintsOid,
value, false, true);
// add extended control to modify request
modifyRequest.Controls.Add(LDAP_SERVER_POLICY_HINTS_OID);
/* send the request into the LDAPConnection and receive the response */
var result = ldapCon.SendRequest(modifyRequest);
}
The call to Password changer is enclosed as follows,
using (var domain = Domain.GetDomain(new DirectoryContext(
DirectoryContextType.DirectoryServer,
ActiveDirectoryInstance,
request.ServiceAccountName,
request.ServiceAccountPassword)))
using (var directoryEntry = domain.GetDirectoryEntry())
using (var directorySearcher = new DirectorySearcher(directoryEntry))
using (var conn = new LdapConnection(new LdapDirectoryIdentifier(ActiveDirectoryInstance),
new NetworkCredential(request.ServiceAccountName,
request.ServiceAccountPassword,
ActiveDirectoryInstance),
AuthType.Ntlm))
{
...
...
PasswordChanger(....)
...
...
}
EDIT:
This is to do with the scenario explained here
https://support.microsoft.com/en-us/kb/2386717/
RE my comment re "As an alternative solution, it would be possible to use "SetPassword" and reset the password to a generated value and then force the user to change it on their next login."
We can't do that in our scenario as we have password history and minimum age restrictions (24h) enabled. Hence I can't use ChangePassword in user context and SetPassword in admin context (as that wouldn't respect password history).
The LDAP_SERVER_POLICY_HINTS_OID control only enforces password history constraints, meaning that you're only preventing password reuse which should be a non-issue if you generate random passwords in the first place.
To test new passwords against password complexity settings, you would need access to the password filters installed on Domain Controllers.
Otherwise you'll need to use SetPassword - it will enforce complexity requirements, but not password history.
Related
The documentation shows how to make a transfer from one wallet to another. In one account.
// Initialize the rpc client and a wallet
var rpcClient = ClientFactory.GetClient(Cluster.TestNet);
var wallet = new Wallet();
// Get the source account
var fromAccount = wallet.GetAccount(0);
// Get the destination account
var toAccount = wallet.GetAccount(1);
// Get a recent block hash to include in the transaction
var blockHash = rpcClient.GetRecentBlockHash();
// Initialize a transaction builder and chain as many instructions as you want before building the message
var tx = new TransactionBuilder().
SetRecentBlockHash(blockHash.Result.Value.Blockhash).
SetFeePayer(fromAccount).
AddInstruction(MemoProgram.NewMemo(fromAccount, "Hello from Sol.Net :)")).
AddInstruction(SystemProgram.Transfer(fromAccount, toAccount.GetPublicKey, 100000)).
Build(fromAccount);
var firstSig = rpcClient.SendTransaction(tx);
How to make a transfer to another account?
Do I need to know the private key of the account to which I will transfer?
Your example has all of the pieces you need. To transfer to another account, you need to create a transaction (using TransactionBuilder), and specifically add a SystemProgram.Transfer instruction to your transaction. Also, in your example, you're sending from fromAccount, which has the private key, to toAccount.PublicKey. You're using the PublicKey of toAccount, so no need for the private key of the recipient.
Note that the example appears to be incorrect, so you should base your code from this example instead: https://github.com/bmresearch/Solnet/blob/8369ac166ed90a7e6b07060178ed70745bd97bc3/src/Solnet.Examples/TransactionBuilderExample.cs#L22
When I access X509Certificate2.PublicKey or X509Certificate2.PrivateKey initialized from an object that was generated with BouncyCastle, I'm getting my domain account locked out (if I do it multiple times). It happens if I run the program on behalf of a local account with the same name but different password. Here is the code:
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace TestCertificateConversion
{
class Program
{
static void Main(string[] args)
{
var certString = GetCertificateString();
var textReader = new StringReader(certString);
var pemReader = new PemReader(textReader);
var bcCert = pemReader.ReadObject() as Org.BouncyCastle.X509.X509Certificate;
var netCert = DotNetUtilities.ToX509Certificate(bcCert);
var netCert2 = new X509Certificate2(netCert);
var publicKey = netCert2.PublicKey; // this locks out domain account
}
private static string GetCertificateString()
{
return #"-----BEGIN CERTIFICATE-----
MIICvDCCAaSgAwIBAgIQANDHl0sFjYUG3j76dYTadzANBgkqhkiG9w0BAQsFADAQ
MQ4wDAYDVQQDDAVwYWNlbTAgFw0xNjAxMDExMjQ4MzdaGA8yMjAwMDEwMTIyNTg0
N1owEDEOMAwGA1UEAwwFcGFjZW0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC5AKAkYnerRUmeAX0Z+aZsX39LXTVZiUd8U6waw7Hzd9E0YA50tHWizfEI
w7IBZwXS0aiXwHqJvrslc3NNs0grwu/iYQl+FGdudKmgXVE7Riu0uFAHo6eFr0dO
o0IP3LS+dPSWarXEBLbymaXRiPJDyHLefvslcSM9UQ2BHOe7dnHh9K1h+XMKTw3Z
/3szAyABBX9hsJU/mc9XjrMNXHJXALSxTfLIPzDrfh+aJtlopWpnb6vQcXwKksyk
4hyVUfw1klhglJgN0LgBGU7Ps3oxCbOqns7fB+tzkBV1E5Q97otgvMR14qLZgc8k
NQrdMl57GaWQJl6mAP1NR1gZt2f1AgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJ
KoZIhvcNAQELBQADggEBAAEz3vJOfqao9QXPWSz8YCjbWG1FeVG0NdYpd422dC2V
Vrzlo5zrkRv5XPhBOY3o81OhUe7iByiiM9suYfXLNxxd29TBGB5amO8Yv1ZX0hS/
zvVF6QS0+zZvOiujVhfHGiJxKypqgaigI6NM80ZDKPzsRPwFLIJiZYwQ7eQUlrpt
WGgFkZC23/mSOkY6VMmO5zugeMoiXRyFq33uWLlaAr+zJtRh1IPRmkA1lJv0bkC1
SslO0oSDoT2lcvZkQ5odFKX5i1z7T/wioQqG62i8nsDSz+iZOqUyDx7bL8fIEHog
qgwizgr2aAPLO/VQKU9pRTyRNFl/GL5bi7w8NN+rLxE=
-----END CERTIFICATE-----";
}
}
}
I'm not sure what I'm doing wrong, are there any security settings I might need to change to prevent it from locking out domain accounts?
Can you check and confirm if the service account is coming in this format
I checked the .net source code and found what causes an authentication problem in X509Certificate2.PublicKey. It is a creation of a new OID object:
public PublicKey PublicKey {
[SecuritySafeCritical]
get {
if (m_safeCertContext.IsInvalid)
throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidHandle), "m_safeCertContext");
if (m_publicKey == null) {
string friendlyName = this.GetKeyAlgorithm();
byte[] parameters = this.GetKeyAlgorithmParameters();
byte[] keyValue = this.GetPublicKey();
Oid oid = new Oid(friendlyName, OidGroup.PublicKeyAlgorithm, true); // this line
m_publicKey = new PublicKey(oid, new AsnEncodedData(oid, parameters), new AsnEncodedData(oid, keyValue));
}
return m_publicKey;
}
}
The OID constructor is called with lookupFriendlyName set to 'true', which leads to FindOidInfoWithFallback function:
// Try to find OID info within a specific group, and if that doesn't work fall back to all
// groups for compatibility with previous frameworks
internal static string FindOidInfoWithFallback(uint key, string value, OidGroup group)
{
string info = FindOidInfo(key, value, group);
// If we couldn't find it in the requested group, then try again in all groups
if (info == null && group != OidGroup.All)
{
info = FindOidInfo(key, value, OidGroup.All);
}
return info;
}
The first FindOidInfo returns null and then it is called second time with OidGroup.All. Eventually it results in cryptAPI call:
CAPIMethods.CryptFindOIDInfo(dwKeyType, pvKey, dwGroupId);
From documentation:
The CryptFindOIDInfo function performs a lookup in the active
directory to retrieve the friendly names of OIDs under the following
conditions:
The key type in the dwKeyType parameter is set to CRYPT_OID_INFO_OID_KEY or CRYPT_OID_INFO_NAME_KEY.
No group identifier is specified in the dwGroupId parameter or the GroupID refers to EKU OIDs, policy OIDs or template OIDs.
It then attempts to authentication with local user account and as a result I'm getting my domain account locked. From the comments to the code I see that the second FindOidInfo call was added for compatibility with older frameworks and potentially I can remove it. Unfortunately there is no easy was to change the code since it is in the framework itself. I may try to inherit the X509Certificate2 object and rewrite PublicKey and PrivateKey, but I don't really like that idea.
I need to create AD users automatically. The problem is, making sure the generated password meets the password policy of AD. I don't know what the policy will be, is there a way to determine that at runtime? This is what I'm using but you can see the complexity is static to length=16 and 4 non-alphanumeric chars and might not always work. I am looking for a way to get the password policy from AD so the generated passwords are correct.
UserPrincipal up = new UserPrincipal(oPrincipalContext);
up.SamAccountName = userId;
up.SetPassword(System.Web.Security.Membership.GeneratePassword(16, 4));
up.Enabled = false;
up.ExpirePasswordNow();
up.Save();
This is what I'm using. I'm getting the password properties using DirectoryEntry/LDAP, then use those to create the password.
DirectoryEntry child = new DirectoryEntry("LDAP://machine/DC=domain,DC=com");
int minPwdLength = (int)child.Properties["minPwdLength"].Value;
int pwdProperties = (int)child.Properties["pwdProperties"].Value;
Use the properties when creating the password.
UserPrincipal up = new UserPrincipal(oPrincipalContext);
up.SamAccountName = userId;
up.SetPassword(System.Web.Security.Membership.GeneratePassword(minPwdLength,
pwdProperties));
up.Enabled = true;
up.ExpirePasswordNow();
up.Save();
I tried remember me with session but I can not succeed. First of all is it possible?
in CheckedChanged method
if (CheckBox1.Checked)
{
Session["email"] = TextBox1.Text;
Session["pass"] = TextBox2.Text;
}
in pageload method
if (Session["email"].ToString() !=null && Session["pass"].ToString() !=null)
{
TextBox1.Text = Session["email"].ToString();
TextBox2.Text = Session["pass"].ToString();
}
but it doesnt work.
You can't use Session for this purpose. You should have to use Cookie to save the Remember Me status.
"Remember me" functionality is implemented by using persistent cookie.
You can't use Session object, because it will be automatically deleted after certain period of user inactivity.
Security note: Never store user password in plaintext format!
Btw: its better that you create a user class with two properties email and pass, and store that in one session element:
User myUser = new User();
myUser.Email = "test#test.com";
myUser.Pass = "123456";
session["user"] = myUser;
and you store the Remember me inside a cookie, with some credentials highly encrypted or salted hash, so when your user visits again you match the name and salted hash with the info you have in your database
change like this. as you are checking for the value. you cant access ToString() method , when null object is there .
if (!string.IsNullOrEmpty(Session["email"]) && string.IsNullOrEmpty(Session["pass"])) )
// then do with session objects.
When I try to update the Name field (corresponds to the CN) on UserPrincipal (Principal, really), I get an error "The server is unwilling to process the request" on the call to UserPrincipal.Save().
I've checked to make sure there isn't another object in the same OU with the same Name (CN).
The PrincipalContext I'm operating at is the domain root (not exactly at the OU level where the user account exists).
What reason might there be for this error? Is it something that might be security policy related (even though I'm able to update all the other fields)?
using (var context = new PrincipalContext(ContextType.Domain, ConfigurationManager.AppSettings["domain"], ConfigurationManager.AppSettings["rootDN"], ContextOptions.Negotiate, ConfigurationManager.AppSettings["username"], ConfigurationManager.AppSettings["password"])) {
var user = UserPrincipal.FindByIdentity(context, IdentityType.Sid, "..."); // SID abbreviated
user.Name = "Name, Test";
user.Save();
}
The user I am using to create the PrincipalContext has the security rights to modify AD objects. If I update any other of the other fields (e.g. Surname, GivenName), everything works fine.
EDIT:
I've been able to accomplish what I need to do (using ADSI), but I have to run the following code under impersonation. The impersonation code is ugly, and the code below breaks away from the other way I'm updating AD data (using DirectoryServices.AccountManagement), so I'd like to get a better solution.
using (var companyOU = new DirectoryEntry("LDAP://" + company.UserAccountOU)) {
companyOU.Invoke("MoveHere", "LDAP://" + user.DistinguishedName, "cn=Name\, Test");
}
This is a cleaner way
using (var context = new PrincipalContext(ContextType.Domain))
{
var group = GroupPrincipal.FindByIdentity(context, groupName);
group.SamAccountName = newGroupName;
group.DisplayName = newGroupName;
group.Save();
var dirEntry = (DirectoryEntry)group.GetUnderlyingObject();
dirEntry.Rename("CN=" + newGroupName);
dirEntry.CommitChanges();
}
The only way I've found to do this is in the EDIT section in my question. Basically, you cannot use the UserPrincipal class. There is something special about the CN attribute, and you need to drop down a level and use DirectoryEntry, an LDAP string, and invoke the "MoveHere" ADSI command to rename the user account.