Tuesday, December 02, 2008

MOSS WebPart to change user password using WinNT

Last week I was faced to following problem: I drive a temporary MOSS Website on a standalone box for a half dozen of external users. Every user has an own local (!) account on the box since there's no domain. It does work well since MOSS supports WinNT authentification.

The local administrator is responsible to create users and provide them with initial passwords. Users can now login on Website. Immediately come the questions like: "How can I change my initial password? I forgot my password, can you reset it?"

As the one and only administrator of the box I can do it only over the weekend, when I get physical access to the machine. It is a little bit annoying for users as well as for me. So I dreamed about self-service for users: just change your own password via dedicated MOSS web page.

After a couple of minutes Web surfing I've understood: there are numerous approaches to change a user password for AD based MOSS farms. Unfortunately they won't work for WinNT authentification.

So I decided to write my own Web part helping the currently logged in user to change her/his password. Additionally there must be an option for administrator to set password for any user, if he/she forgot it.

Thanks to the MOSS Web part framework and .NET Framework at all, the work was done in couple of hours:

  • Implement a routine to check if the currently logged in user belongs to local administrators group.

  • Override CreateChildrenControls() to build the UI for desired Web part. We need:
    • textbox for username
      • set this box to read-only if currently logged in user is not a local admin (simple users cannot change passwords of other users)
      • let the local admin to put any user name here to be able to change password for any other user
    • textbox for old password
    • textbox for new password
    • textbox for new password repeated (to avoid typos in new password)
    • button to invoke change password operation
    • labels for all the textboxes to guide the user input
    • a control to display result of the operation (I used another label control)
    • a couple of literals with line break to align UI elements

  • Implement click handler for the button invoking the password change. Here's some logic behind the scenes: if the content of the username textbox is the same as login name of the currently logged in users, the change password routine should be invoked. The old password and new password are required for changing the password.
    If the content of the username textbox differs from the login name of the currently logged in users, the set password routine should be invoked (currently logged in user is a local administrator and wants to set a forgotten password for another Web site user). Sounds more complicated as implementation.

  • Both change password and set password routines are easily to invoke using built-in .NET support for Active Directory: event using WinNT as authentification provider. System.DirectoryServices is the namespace, and DirectoryEntry is the class one needs to use to achieve the goal.
    The standard ADSI scenario: Find, Bind, Edit, Save – does work here. To set or change password for a user, the user object must be bound. The connection string has usually the form: "WinNT ://<servername>/<username>,User", where servername is the authentification NT machine name, and username is the user's login name.
    Both names can be passed out of username obtained from identity of current thread.

  • Being skeptical about trouble-free work of the part, one may want a rudimentary tracing functionality just dropping tracing messages in a local text file. This functionality can help on troubleshooting but must be flexible enough to be turned on or off.

  • In common there's not much to configure the Web part. Nevertheless two configuration options were implemented in the Web part:
    • Provider connection string (standard value "WinNT://<servernbame>/<username>,User" as described above)
    • Pathname of the log file: additionally as toggle for tracing – no tracing on null or empty log file pathname

    Both options were implemented as Web part editable parameters to provide the Web site administrator with comfortable configuration integrated into standard MOSS Web page edit mode.

    That's all. Check out the code and feel free to ask for more information (e. g. how to build a MOSS solution with this Web part, how to localize etc.


    Enjoy!


    WebPart as .wsp file available on request.

    Code:


using System;

using System.Diagnostics;

using System.DirectoryServices;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using Microsoft.SharePoint.WebControls;

using System.Security.Principal;

using System.Threading;



namespace MBecker.SharePoint.Web

{


public
class
MBChangePasswordWebPart : WebPart

{


///
<summary>


/// WebPart title


///
</summary>


public
const
string Title = "Web Part to help users to change their passwords.";



///
<summary>


/// Logfile pathname


///
</summary>


private
string logFileName = "";



///
<summary>


/// Directory entry string to access user object


///
</summary>


private
string userEntryString = "";



///
<summary>


/// WebPart property: Provider connection string.


/// To be configured in webpart modification mode.


///
</summary>

[WebBrowsable]

[WebDisplayName("Provider connection string")]

[WebDescription("Provider connection string to get the user object for password changes; {0} is the placeholder for user login, {1} is the placeholder for domain/server name.")]

[Personalizable(PersonalizationScope.Shared)]


public
string UserEntryString

{


get

{


return userEntryString;

}


set

{

userEntryString = value;

}

}



///
<summary>


/// WebPart property: Logfile pathname.


/// To be configured in webpart modification mode.


///
</summary>

[WebBrowsable]

[WebDisplayName("LogFile Pathname")]

[WebDescription("Quick switch to toggle tracing on/off; empty logfile pathname assumes no tracing required")]

[Personalizable(PersonalizationScope.Shared)]


public
string LogFilename

{


get

{


return logFileName ;

}


set

{

logFileName = value;

}

}




///
<summary>


/// Mastering outlook of the WebPart with required child controls, required.


///
</summary>


protected
override
void CreateChildControls()

{


if (Page.IsPostBack)

{


}


else

{


}



// Add webpart controls


try

{


// Label for username


Label labelUsername = new
Label();

labelUsername.Text = Resource.labelUser;

labelUsername.CssClass = "changePasswordLabel";

Controls.Add(labelUsername);



Literal br1 = new
Literal();

br1.Text = "<br/>";

Controls.Add(br1);



// Textbox to enter username


// will be displayed as readonly if current user is not an local administrator


TextBox textBoxUsername = new
TextBox();

textBoxUsername.Text = System.Threading.Thread.CurrentPrincipal.Identity.Name;

textBoxUsername.ReadOnly = !IsAdminMode();

textBoxUsername.CssClass = "changePasswordTextBox";

textBoxUsername.ID = "textBoxUsername";

Controls.Add(textBoxUsername);



Literal br2 = new
Literal();

br2.Text = "<br/>";

Controls.Add(br2);



// Label for old password


Label labelOldPassword = new
Label();

labelOldPassword.Text = Resource.labelOldPassword;

labelOldPassword.CssClass = "changePasswordLabel";

Controls.Add(labelOldPassword);



Literal br3 = new
Literal();

br3.Text = "<br/>";

Controls.Add(br3);



// Password textbox to enter the old password


PasswordTextBox textBoxOldPassword = new
PasswordTextBox();

textBoxOldPassword.ID = "textBoxOldPassword";

textBoxOldPassword.CssClass = "changePasswordTextBox";

Controls.Add(textBoxOldPassword);



Literal br4 = new
Literal();

br4.Text = "<br/>";

Controls.Add(br4);



// Label for new password


Label labelNewPassword1 = new
Label();

labelNewPassword1.Text = Resource.labelNewPassword1;

labelNewPassword1.CssClass = "changePasswordLabel";

Controls.Add(labelNewPassword1);



Literal br5 = new
Literal();

br5.Text = "<br/>";

Controls.Add(br5);



// Password textbox to enter new password


PasswordTextBox textBoxNewPassword1 = new
PasswordTextBox();

textBoxNewPassword1.ID = "textBoxNewPassword1";

textBoxNewPassword1.CssClass = "changePasswordTextBox";

Controls.Add(textBoxNewPassword1);



Literal br6 = new
Literal();

br6.Text = "<br/>";

Controls.Add(br6);



// Label for new password repeated


Label labelNewPassword2 = new
Label();

labelNewPassword2.Text = Resource.labelNewPassword2;

labelNewPassword2.CssClass = "changePasswordLabel";

Controls.Add(labelNewPassword2);



Literal br7 = new
Literal();

br7.Text = "<br/>";

Controls.Add(br7);



// Passowrd textbox to enter new password repeated


PasswordTextBox textBoxNewPassword2 = new
PasswordTextBox();

textBoxNewPassword2.ID = "textBoxNewPassword2";

textBoxNewPassword2.CssClass = "changePasswordTextBox";

Controls.Add(textBoxNewPassword2);



Literal br8 = new
Literal();

br8.Text = "<br/><br/>";

Controls.Add(br8);



// Button to invoke password change


Button buttonSetPassword = new
Button();

buttonSetPassword.Text = Resource.buttonSetPassword;

buttonSetPassword.Click += new
EventHandler(buttonSetPassword_Click);

buttonSetPassword.ID = "buttonSetPassword";

buttonSetPassword.CssClass = "changePasswordButton";

Controls.Add(buttonSetPassword);



Literal br9 = new
Literal();

br9.Text = "<br/><br/>";

Controls.Add(br9);



Label labelResult = new
Label();

labelResult.ID = "labelResult";

labelResult.CssClass = "changePasswordResultLabel";

Controls.Add(labelResult);


}


catch (Exception ex)

{


// Adding child controls fired an exception


Debug.Assert(false, Resource.exceptionCreateChildControls + ex.Message + "\r\n" + ex.StackTrace);

trace(Resource.exceptionCreateChildControls + ex.Message + "\r\n" + ex.StackTrace);

}



base.CreateChildControls();


}



///
<summary>


/// Button click handler, invokes change/set password


///
</summary>


///
<param name="sender">Button object</param>


///
<param name="e">Click event argumets</param>


void buttonSetPassword_Click(object sender, EventArgs e)

{


string result;


try

{

((Button)this.FindControl("buttonSetPassword")).Enabled = false;

((Label)this.FindControl("labelResult")).Text = "";


try

{


bool bSetPassword = false;


string userName = ((TextBox)this.FindControl("textBoxUsername")).Text;



// Check if set or change password operation should be invoked


// change password - to change password for currently logged in user


// set password - to change password for username entered in username textbox


// (only if currently logged in user is a local admin)

bSetPassword = (userName != System.Threading.Thread.CurrentPrincipal.Identity.Name);


string serverName = "";


if (userName.IndexOf('\\') != -1)

{


// Parse user name to split server name and user name

serverName = userName.Substring(0,userName.IndexOf('\\'));

userName = userName.Substring(userName.IndexOf('\\')+1);

}


if (!bSetPassword)

{


// Change password for currently logged in user

result = changePassword(

serverName,

userName,

((PasswordTextBox)this.FindControl("textBoxOldPassword")).Password,

((PasswordTextBox)this.FindControl("textBoxNewPassword1")).Password,

((PasswordTextBox)this.FindControl("textBoxNewPassword2")).Password

);

}


else

{


// Set password for a username entered in textbox

result = setPassword(

serverName,

userName,

((PasswordTextBox)this.FindControl("textBoxNewPassword1")).Password,

((PasswordTextBox)this.FindControl("textBoxNewPassword2")).Password

);

}

}


catch (Exception ex)

{


Debug.Assert(false, Resource.exceptionSetChangePassword1 + ex.Message + "\r\n" + ex.StackTrace);

trace(Resource.exceptionSetChangePassword1 + ex.Message + "\r\n" + ex.StackTrace);

result = ex.Message + "\r\n" + ex.StackTrace;

}

((Label)this.FindControl("labelResult")).Text = result;

((Button)this.FindControl("buttonSetPassword")).Enabled = true;

}


catch (Exception ex)

{


Debug.Assert(false, Resource.exceptionSetChangePassword2 + ex.Message + "\r\n" + ex.StackTrace);

trace(Resource.exceptionSetChangePassword2 + ex.Message + "\r\n" + ex.StackTrace);

}

}



///
<summary>


/// Check if the currently logged in user is a local admin


///
</summary>


///
<returns>True if the currently logged in user is a local admin, false otherwise</returns>


private
bool IsAdminMode()

{


bool bRet = false;


string groupAdministrators = "Administrators";



//if (System.Threading.Thread.CurrentPrincipal.Identity.Name.IndexOf("mbecker") != -1) bRet = true; // :-)


AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

bRet = Thread.CurrentPrincipal.IsInRole(groupAdministrators);

trace(


String.Format("{0} is " + (bRet?"":" not ") + "in role {1}.",


Thread.CurrentPrincipal.Identity.Name,

groupAdministrators

)

);


return bRet;

}



///
<summary>


/// Change user password


///
</summary>


///
<param name="serverName">Server name</param>


///
<param name="userName">User Loginname</param>


///
<param name="oldPassword">Old password</param>


///
<param name="newPassword1">New password</param>


///
<param name="newPassword2">New password repeated</param>


///
<returns>State string to display on webpart</returns>


private
string changePassword(


string serverName,


string userName,


string oldPassword,


string newPassword1,


string newPassword2)

{


string result = Resource.textPasswordChanged;


if (newPassword1 != newPassword2)

{


// new password and new password repepated do not match, break operation

result = Resource.testPasswordNotChanged + Resource.exceptionPasswordDoNotMatch;

}


else

{


try

{


// Change user password as seen in numerous ADSI samples


DirectoryEntry myDirectoryEntry;


string directoryEntryString = String.Format(UserEntryString, userName, serverName);

trace(directoryEntryString);

myDirectoryEntry = new
DirectoryEntry(directoryEntryString);


string[] passwords = new
string[2];

passwords[0] = oldPassword;

passwords[1] = newPassword1;

myDirectoryEntry.Invoke("ChangePassword", passwords);

}


catch (Exception ex)

{


// Change password failed

result = Resource.testPasswordNotChanged + ex.StackTrace;


while (ex != null)

{

trace(ex.Message);

trace(ex.StackTrace);

ex = ex.InnerException;

}

}

}


return result;

}



///
<summary>


/// Set user password


///
</summary>


///
<param name="serverName">Server name</param>


///
<param name="userName">User Loginname</param>


///
<param name="newPassword1">New password</param>


///
<param name="newPassword2">New password repeated</param>


///
<returns>State string to display on webpart</returns>


private
string setPassword(


string serverName,


string userName,


string newPassword1,


string newPassword2)

{


string result = Resource.textPasswordChanged;


if (newPassword1 != newPassword2)

{


// new password and new password repepated do not match, break operation

result = Resource.testPasswordNotChanged + Resource.exceptionPasswordDoNotMatch;

}


else

{


try

{


// Set user password as seen in numerous ADSI samples


DirectoryEntry myDirectoryEntry;


string directoryEntryString = String.Format(UserEntryString, userName, serverName);

trace(directoryEntryString);

myDirectoryEntry = new
DirectoryEntry(directoryEntryString);

myDirectoryEntry.Invoke("SetPassword", newPassword1);

}


catch (Exception ex)

{


// Set password failed

result = Resource.testPasswordNotChanged + ex.StackTrace;


while (ex != null)

{

trace(ex.Message);

trace(ex.StackTrace);

ex = ex.InnerException;

}

}

}


return result;

}



///
<summary>


/// Trace diagnostic information in configured log file.


/// Ensure access rights to the file/folder to avoid exceptions due to


/// file access failures


///
</summary>


///
<param name="msg">Trace message string</param>


private
void trace(string msg)

{


if (!String.IsNullOrEmpty(LogFilename))

{


// Trace message if a logfile specified


try

{

System.IO.StreamWriter sw = new System.IO.StreamWriter(LogFilename, true);

sw.WriteLine(DateTime.Now.ToString() + "\t" + msg);

sw.Flush();

sw.Close();

}


catch (Exception ex)

{


Debug.Assert(false, String.Format(Resource.exceptionTrace, msg) + ex.Message + "\r\n" + ex.StackTrace);

}

}

}

}

}