Sunday, March 1, 2015

Active Directory SetPassword or ChangePassword Possible Errors


When we try to update the user password on an Active Directory (AD or AD LDS) environment, we usually read on the documentation about the basic steps that are needed to set or change the password to a user. If we follow those basic steps, we would be very lucky of all goes well. There are often several other requirements that need to be met to be able to set the password. Some of these include:
  • The password is clear text
  • The password does not meet the minimum length requirement
  • The password does not meet the format like numeric or capitalization (password policy)
  • The Authentication type is not high enough


Any of these items can cause COM error similar to this:

“Exception has been thrown by the target of an invocation COMException (0x80005009)

To illustrate this, let’s looks at what the basic code to set a user’s password looks like:

//add these setting on the configuration file
        const string adConnection = "ldap://mydomain:389/CN=ORG,DC=OZKARY,DC=COM";
        const string adUsername = "someuser"//user with admin access to AD
        const string adPw = "*******";

        public static void SetPassword(string username, string password){

            using (var de = new DirectoryEntry(adConnection, adUsername, adPw))
            {
                //Win Server 2008 schema or less uses sAMAccount 
                //not userPrincipalName
         string query = String.Format("(&(objectCategory=person)(objectClass=user)(&(userPrincipalName={0}))", username);           

                using (var ds = new DirectorySearcher(de))
                {
                    ds.SearchScope = System.DirectoryServices.SearchScope.Subtree;
                    ds.Filter = query;
                    SearchResult res = ds.FindOne();

                    if (res != null)
                    {
                        using (var userEntry = res.GetDirectoryEntry())
                        {
                            userEntry.Invoke("SetPassword", new object[] 
                            { password });                           
                        }

                    }
                }
            }
        }

If we are lucky, this should be enough for the operation to be successful, but we are probably getting errors, and that is why we are writing this article.  The first thing to do is to figure out more about this error by extracting the COM exception error code and mapping them to a root cause. This is where this function may be helpful:

  private static void HandleComException(TargetInvocationException tie)
        {          
            if (tie != null && tie.InnerException is COMException)
            {
                COMException ce = (COMException)tie.InnerException;
                int errorCode = ce.ErrorCode;
                string message = ce.Message;
                message += ce.InnerException != null ? ce.InnerException.Message :                                      string.Empty;
                Trace.TraceError(message);
                message = "error_not_found";

                // if the exception is due to password not meeting complexity requirements, 
                // then return ProviderException                
                if ((errorCode == unchecked((int)0x800708c5)) 
                    || (errorCode == unchecked((int)0x8007202f)) 
                    || (errorCode == unchecked((int)0x8007052d)) 
                    || (errorCode == unchecked((int)0x8007052f)))
                {
                    message = "password_not_complex";//password policy not met
                }
                else if ((errorCode == unchecked((int)0x8000500d)))
                {
                    message = "no_secure_conn_for_password";//need higher than security=None
                }
                else if ((errorCode == unchecked((int)0x80072035)))
                {
                    //min password length or the password was used before
                    //The server is unwilling to process the request
                    message="min_password_length_used_before";
                }
                else if ((errorCode == unchecked((int)0x80005009)))
                {
                    message="password_clear_text";//need to set option to use clear text
                }

                throw new ProviderException(message, ce);//need to match this error
            }
           
        }
The exception usually is of type TargetInvocation, but it does not provide enough information to determine the problem. The function reads the inner exception (COM exception) and compare the error code to the possible Active Directory Service Interface (ADSI) errors. As you can see there are other possible errors, but we are only handling the ones that are specific to a password change.

The password not complex and the password length errors basically mean that the new password is not meeting the password policy requirements. We need to find out about those requirements from the AD administrator and make sure that the new password meets them before we attempt to set the password.

Some requirements may be:
  • Length of 8 characters
  • At least one capital letter or number
  • One special character

The unsecured connection and clear text errors can be addressed by making modifications to the code. When we try to update the password for a user, we can’t use a Secure=None level on the authentication type. We need to provide a higher security by adding these lines of code:

AuthenticationTypes auth = AuthenticationTypes.Signing | AuthenticationTypes.Sealing 
AuthenticationTypes.Secure;          
using (var de = new DirectoryEntry(adConnection, adUsername, adPw,auth))

The clear text error is due to the fact that the password is not hashed, so we need to indicate that this would be OK by adding this line of code:

userEntry.Options.PasswordEncoding = PasswordEncodingMethod.PasswordEncodingClear;

Once we have added those changes, the password update should be able to work or we should at least have added instrumentation in our code to help identified the problem. This is what the new version looks like:

public static void SetPasswordNew(string username, string password)
        {
            AuthenticationTypes auth = AuthenticationTypes.Signing |  AuthenticationTypes.Sealing                                        | AuthenticationTypes.Secure;          
            using (var de = new DirectoryEntry(adConnection, adUsername, adPw,auth))
            {
                //Win Server 2008 schema or less uses sAMAccount not userPrincipalName. 
                //add to config file
string query = String.Format("(&(objectClass=user)(&(userPrincipalName={0}))",                      username);           

                using (var ds = new DirectorySearcher(de))
                {
                    ds.SearchScope = System.DirectoryServices.SearchScope.Subtree;
                    ds.Filter = query;
                    SearchResult res = ds.FindOne();

                    if (res != null)
                    {
                        try
                        {
                            using (var userEntry = res.GetDirectoryEntry())
                            {
                                userEntry.Options.PasswordEncoding =                                 PasswordEncodingMethod.PasswordEncodingClear;    
                                userEntry.Invoke("SetPassword", new object[] { password });
                            }
                        }
                        catch (TargetInvocationException tie)
                        {
                            HandleComException(tie);
                        }
                        catch (Exception ex)
                        {
                            Trace.TraceError(ex.Message); //handle this other error
                          
                        }
                       
                    }
                }
            }
        }


By adding these modifications, the password update should be successful.

Hope this helps and thanks.

Saturday, February 21, 2015

The Foreign Key on table with columns could not be created because the principal key columns could not be determined

The Foreign Key on table with columns could not be created because the principal key columns could not be determined. Use the AddForeignKey fluent API to fully specify the Foreign Key.

This error takes place when the code first model is trying to create a foreign key to a table that is not included in the model. Since Entity Framework (EF) can’t find the model that contains the primary key association, it raises the error. 

To enable a foreign key to a non-existent model in you project, just use the fluent API to add or remove the constraint. This enables us to basically run SQL queries on the target database. To illustrate this problem, let’s take a look at the following model:

    [Table("App")]
    public partial class App
    {      
        [Key]
        public int Id { get; set; }      

        [Required]
        [StringLength(150)]
        public string Name { get; set; }

        /// <summary>
        /// add this field as a FK to the dbo.Roles table
        /// which does not exist in my model
        /// </summary>       
        public string RoleId { get; set; }       
    }

We want to create a foreign key on the RoleId field, but we do not have the dbo.Roles table model defined in the project. The migration code attempts to do that by just calling the fluent API  ForeignKey  as shown below:

CreateTable(
                "dbo.App",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(nullable: false, maxLength: 150),
                        RoleId = c.String(),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.MyRoles", t => t.RoleId).Index(t => t.RoleId);

The highlighted line generates the error because the mapping between RoleId to the primary key on the dbo.Roles table is not defined or is unknown by EF.  To make it explicit, we can change the code as the error suggests to using the AddForeignKey API and removing the fluent method ForeignKey as follows:

CreateTable(
                "dbo. App",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(nullable: false, maxLength: 150),
                        RoleId = c.String(),
                    })
                .PrimaryKey(t => t.Id).Index(t => t.RoleId);;

AddForeignKey("dbo.App", "RoleId", "dbo.Roles","Id");

We should notice how on this method we are explicitly indicating that we want to map the RoleId column to the Primay key column of Id on the Roles table.

We can now build the code and run the database update preview to see what SQL script would be executed by using this command:

Update-Database  -script

Note: Run this on the Package Manager Console

Thanks for reading.



Wednesday, February 11, 2015

Database Project build fails Could not load file or assembly Microsoft.SqlServer.TransactSql, Version=12.0

This error can take place when Visual Studio 2013 is updated to Update 4 and extended Transact-SQL verification is enabled on the database project.

Error message:

System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.SqlServer.TransactSql, Version=12.0.0.0

To fix this issue, uncheck the Enable extended Transact-SQL verification option from the project properties.  If you wonder why this happened, the feature was deprecated and has been removed from the latest SQL Server tooling update which is what takes place when using VS2013 Update 4.

Database Project Properties

Microsoft Reference VS2013 Update 4:

Sunday, February 1, 2015

ActiveDirectory Membership Provider a referral was returned from the server

The “A referral was returned from the server” error usually means that there is a conflict between the IP address and the domain controller (DC) defined on the connection string.

To illustrate the problem, we define two IP addresses hosted on different domains:

IP Address
DC Name
Notes
172.1.1.10
ozkary.com
Production domain

172.1.30.50
ozkaryDev.com
Development domain


If we defined a LDAP connection string with this format:

LDAP://172.1.1.10:389/OU=USERS,DC=OZKARYDEV,DC=COM

This will generate the error because the IP is actually on the OZKARY DC not the OZKARYDEV DC. To correct the problem, we would need to use either one of these valid connections:

Connection String
Comments
LDAP://172.1.1.10:389/OU=USERS,DC=OZKARY,DC=COM
Production setting

LDAP://172.1.30.50:389/OU=USERS,DC=OZKARYDEV,DC=COM
Development Setting



That should address the problem.