A Better ASP.Net Member/Role Management Page Pt. 5
Prologue
**Author Note – If you are bewildered by a line (perhaps two) that are red and bold, don’t they that they’re errors, they are corrections/additions. Please see the comments, after the article for comments regarding the highlighted line(s).**
In previous posts we started creating “A Better ASP.Net Member/Role Management Page.” In part 1, we set criteria, and in part 2, part 3 and part 4 we created functionality to add, delete and edit users. We’ve finished nearly all the functionality for editing users with one exception, changing usernames. We’ll be addressing that in this post.
Problem
Changing a username is functionality that isn’t included in the default Microsoft membership provider. The reason for this, in my understanding, is that changing usernames is considered somewhat of a security issue. Really,i don’t understand why it isn’t included, changing a username could be put together in such a way that it is reasonably secure, you know require passwords and or security questions etc. like practically everything else that you try to change using the membership provider does. Although the functionality isn’t included in the Microsoft provider, we can add the functionality on our own (and it’s actually relatively easy).
Solution
Unfortunately, this functionality will be the one part that we won’t be able to contain fully in our page, we’ll have to add a stored procedure to our database. Well, I guess we could put all our SQL stuff in a subroutine, but that’s leaves us with a security problem, accepting user in put and just inserting into a SQL statement and running it directly form our page leaves us open for SQL injection attack. The most secure thing would be to create a stored procedure in the database, and like I said that means that we don’t have a fully self contained page.
To workaround this limitation, what we’ll do is add a property to the page so that we can indicate that we have created the stored procedure and we can change the username. This will turn on/off the ‘change username’ link on our page.
Let’s start with the SQL stored procedure. If you’ve ever looked at a database that’s enabled for the ASP.net membership provider you will notice that enabling a database for ASP.Net membership provider adds a number of stored procedures. We’ll add another stored procedure and we’ll use the same naming convention. To add the stored procedure execute the following SQL code:
CREATE PROCEDURE aspnet_Membership_ChangeUsername
@currentUsername nvarchar(255),
@newUsername nvarchar(255),
@results bit OUTPUT
AS
UPDATE aspnet_Users
SET UserName = @newUsername,
LoweredUserName = LOWER(@newUsername)
WHERE LOWER(UserName) = LOWER(@currentUsername)
set @results = @@ROWCOUNT
The stored procedure here will take three parameters, one for each of the usernames, the new and the old, and one parameter to output the success of the operation. It will return 0 on failure and 1 on success (as one row was changed on success). The actual username data is actually only stored in one location, in the user table. Rather than using the username as the ID to link with all other tables, there’s a unique UserID in the user table that is used. Changing the username is easy, we have two places to change it in one table and it doesn’t take much.
Once that’s done, we need to add the functionality to our page. We don’t have much to add to the front-end code, just the change username dialog and a new style. Add the following to your style section:
.taken
{
color: Red;
}
and the following to your front end code. Place it at the end of the page, but before our closing tag for our pnlFormatEverything panel:
<%-- Change Username Dialog --%>
<asp:UpdatePanel id="upnlChangeUsername" runat="server" ChildrenAsTriggers="true">
<ContentTemplate>
<asp:Panel ID="pnlChangeUsername" runat="server"
style="display: none;
text-align:center;" CssClass="modalPopup">
<b>Enter a New Username</b><br /><br />
<asp:Table runat="server" ID="tblChangeUsername">
<asp:TableRow>
<asp:TableCell HorizontalAlign="Right">
Current Username:
</asp:TableCell>
<asp:TableCell HorizontalAlign="Left">
<asp:Label ID="lblCurrentUsername"
runat="server" Text="" />
</asp:TableCell>
<asp:TableCell></asp:TableCell>
</asp:TableRow>
<asp:Tablerow>
<asp:TableCell HorizontalAlign="Right">
New Username:
</asp:TableCell>
<asp:TableCell HorizontalAlign="Left">
<asp:TextBox ID="txtNewUsername" runat="server" />
<div runat="server" id="divUserAvailablity"
class="taken"></div>
<cc1:TextBoxWatermarkExtender
ID="tbeNewUsername" runat="server"
TargetControlID="txtNewUsername"
WatermarkText="Enter New Username"
WatermarkCssClass="watermarked">
</cc1:TextBoxWatermarkExtender>
</asp:TableCell>
<asp:TableCell>
<asp:RequiredFieldValidator runat="server"
ID="rfvNewUsername"
ControlToValidate="txtNewUsername"
Display="Dynamic"
ValidationGroup="ChangeUsername"
ErrorMessage="You must enter a new username"
Text="*" />
</asp:TableCell>
</asp:Tablerow>
<asp:TableRow>
<asp:TableCell ColumnSpan="3">
<asp:ValidationSummary ID="vsNewUsername"
ValidationGroup="ChangeUsername" runat="server"
DisplayMode="List" ShowSummary="true"
EnableClientScript="true" />
</asp:TableCell>
</asp:TableRow>
</asp:Table>
<asp:LinkButton ID="lnkChangeUsernameSave"
runat="server" Text="Save"
CausesValidation="true" ValidationGroup="ChangeUsername" />
<asp:LinkButton ID="lnkChangeUsernameCancel"
runat="server" Text="Cancel" />
</asp:Panel>
<asp:Button ID="btnFakeShoeMPEChangeUsernameDialogButton"
runat="server" style="display:none;" />
<cc1:ModalPopupExtender ID="mpeChangeUsername" runat="server"
TargetControlID="btnFakeShoeMPEChangeUsernameDialogButton"
PopupControlID="pnlChangeUsername"
BackgroundCssClass="modalBackground">
</cc1:ModalPopupExtender>
</ContentTemplate>
</asp:UpdatePanel>
Basically we’re just creating a label to put in the existing username, a textbox for the new username and then save and cancel buttons. We also add a div so that we can tell the user if the new username is already being used.
Now let’s add the back end code. First, we’ll add another entry to our SetUI enumeration, and another case to our SetUI case statement. Add the following line to our enumeration:
ChangeUsername
and add the following case statement to our SetUI Case statement:
Case SetUIModes.ChangeUsername
mpeChangeUsername.Show()
Next, we’ll hook up the ‘Change Username’ LinkButton. It’s functionality is pretty easy. Add the following to the our lnkChangeUsername_Click event handler:
lblCurrentUsername.Text = lblUsernameEdit.Text
SetUI(SetUIModes.ChangeUsername)
We set the current username and then show the dialog.
We don’t need to do anything with the cancel button as it will function correctly without help, however we need to hook up the functionality for the Save button. We’ll want to do a little checking to see if the username is being used before we actually do any changing. We’ll want to display a message that it’s taken if it is. We’ll also want to make sure that the username was successfully changed in the database when we actually execute the SQL. Well add two helper functions, the first to check username availability. Add the following function:
Private Function CheckUsernameIsAvailable( _
ByVal sUsernameToCheck As String) As Boolean
If IsNothing(Membership.GetUser(sUsernameToCheck)) = False Then
Return False
Else
Return True
End If
End Function
Basically, we receive the new username and then check to see if we can retrieve a MembershipUser object from the database. If we can, then the username is NOT available and we return false.
We’ll also want to create a helper function to perform the actual database work, since our membership provider doesn’t do this for us. Add the following helper function (I apologize in advance for dividing of the lines in such a terrible manner…):
Private Function ChangeUsername(ByVal sCurrentUsername As String, _
ByVal sNewUsername As String, _
Optional ByVal iOrdinalForConnectionString As Integer = 0) As Boolean
Using cn As New SqlConnection( _
ConfigurationManager.ConnectionStrings( _
iOrdinalForConnectionString).ConnectionString)
Dim cmd As New SqlCommand _
("aspnet_Membership_ChangeUsername", cn)
cmd.CommandType = Data.CommandType.StoredProcedure
cmd.Parameters.Add _
("@currentUsername", Data.SqlDbType.NVarChar).Value _
= sCurrentUsername
cmd.Parameters.Add _
("@newUsername", Data.SqlDbType.NVarChar).Value _
= sNewUsername
cmd.Parameters.Add("@Results", Data.SqlDbType.Bit).Direction _
= Data.ParameterDirection.Output
cn.Open()
Try
cn.Open()
cmd.ExecuteNonQuery()
Catch ex As Exception
Return False
Finally
cn.Close()
End Try
Return cmd.Parameters("@Results").Value
End Using
End Function
This function is easily the least flexible in the entire object. Notice we pass in both the old and the new usernames. We also pass in an ordinal for the connectionString to use. Unfortunately we are unable to determine the connectionString currently being used in the membership provider, so we have to pass in the ordinal of the connection string we are using. In my case, I don’t even have a connectionString as I’m using the ASP.Net default sqlexpress database, so an ordinal of 0 works for me. You may need to play with what you pass in depending on your connectionstring setup. We use this connectionString to create a connection. With this connection, we create a command object, load it up with our parameters, including an output parameter. We then attempt to execute the stored procedure. If it fails we return false, otherwise we return the success/failure results from the actual database execution. You’ll notice that we use some SQL Data client objects so we’ll need to import the following namespace (or fully qualify all your SQL objects). Add the following import statement to the top of your document:
Imports System.Data.SqlClient
With our helper functions out of the way, we can now concentrate on our save routine. Add the following to your lnkChangeUsernameSave_Click event handler:
If CheckUsernameIsAvailable(txtNewUsername.Text) = False Then
divUserAvailablity.InnerText = "Username taken, sorry."
SetUI(SetUIModes.ChangeUsername)
Exit Sub
End If
If ChangeUsername(lblCurrentUsername.Text, txtNewUsername.Text) = True Then
SetDataSource(Membership.GetAllUsers(), USERS_DATA_SOURCE)
BindGrid(gvManageUsers, USERS_DATA_SOURCE, False)
lblMessageDialog.Text = "Username successfully changed."
Else
lblMessageDialog.Text = "Username change failed."
End If
txtNewUsername.Text = String.Empty
divUserAvailablity.InnerText = String.Empty
SetUI(SetUIModes.MessageDialog)
First we check to make sure the username is available. If it isn’t, then we’ll put up a message much like our validation letting the administrator know that it’s taken. We’ll make sure our change username dialog is still up and then exit the sub. If it’s valid, we attempt to perform the actual change. If the change is successful, we’ll refresh our data source, and then update our GridView with the new username information. Whether it fails or is successful, we’ll set the message dialog, and then when all is said and done, show the message. We’ll also reset our new username TextBox and our availability div so that they are both blank and ready to use next time.
Ok, so we’ve got all our functionality working, but what if a user of our control didn’t put the stored procedure into the database? We’ll need to disable the functionality in that case. Actually, what we’ll do is disable the functionality be default, and then add it back in if the control’s user specifically enables it. We’ll add a property to our user control that will control the ‘Change username’ link so that the functionality to call the DB is only available if the control’s user specifically configures it to say that the DB has been setup. Add the following to define our property:
Public Enum ChangeUsernameSettings
DbIsSetup
DbIsNotSetup
DisableLink
End Enum
Private _ChangeUsernameFunctionality As ChangeUsernameSettings _
= ChangeUsernameSettings.DbIsNotSetup
Public Property ChangeUsernameFunctionality()
Get
Return _ChangeUsernameFunctionality
End Get
Set(ByVal value)
_ChangeUsernameFunctionality = value
End Set
End Property
Here, we create a enumeration for our different settings. Next we create a private variable to hold our property’s current setting, and set it by default to be DbIsNotSetup. Finally we create our property.
Now we’ll need to implement some things so that the settings are used. Add the following line to your SetupEditUser subroutine (anywhere will work):
If _ChangeUsernameFunctionality = ChangeUsernameSettings.DisableLink _
Then lnkChangeUsername.Enabled = False
If disableLink is selected, then we’ll disable it during our EditUser dialog setup. We could add a tooltip if desired, letting the administrator using the page what needs to be done to add the functionality. Next add the following to your lnkChangeUsername_Click event handler (apologies for the big block of text in the middle that has no link breaks (for readability)):
If _ChangeUsernameFunctionality = _
ChangeUsernameSettings.DbIsNotSetup Then
lblMessageDialog.Text = _
"For this fundtion to be available, the following must be met: <br> 1. The database needs to have a stored procedure created for this functionality and <br> 2. the administrator must explicitly control to specify that the functinality is in the database.<br><br> Please see the documentation for more information."
SetUI(SetUIModes.MessageDialog)
Exit Sub
End If
If our page creator doesn’t specifically enable on this functionality, the administrator using our control will receive a message letting them know what needs to happen for the functionality to be enabled. And of course, if we set the property to DbIsSetup, then we’ll have our functionality back.
Epilogue
Ugh… as in ugly. Truly, this isn’t the cleanest portion of our functionality, we are completely disregarding just about every one of our rules about keeping this control self contained and trying to make it useful across the board. But, in our defense, we’re making up for functionality limitations built into the the .Net framework (or is it limitations NOT built into…?). We could of course extend, or rewrite our membership provider, but I choose not to do it that way.
This should fulfill all the requirements we had previously for our membership portion of our management page. Now we just need to work on the roles administration portion. That will be the subject of the next post(s).