Introduction
Many distributed applications need user verification capabilities. Certainly there are plenty of ways to make your own authentication mechanisms for Smart Client, disconnected or ClickOnce applications. However, Microsoft introduced membership and role providers for ASP.NET 2.0, which proved to be popular and useful in certain scenarios for interchangeable security mechanisms. Unfortunately, these providers were not officially supported in Windows client applications. Some people found workarounds, but now with .NET 3.5 and Visual Studio 2008, the membership providers are officially supported and integrated into the architecture.
This is an example walkthrough of how to make a .NET 3.5 Windows application utilize ASP.NET membership and role provider services, a feature now known as Client Application Services. For our purposes, I will use the SQL Server provider for speed and ease of setup, but any provider will work (though creating custom providers is beyond the scope of this tutorial). Terms to know:
- Provider: a pluggable structure that can use any object adhering to a provider interface
- Membership provider: user store and management
- Role provider: access rules store and management
The overall solution will involve a SQL Server database, an ASP.NET web application (with AJAX) and a Windows application. The database will store users and access rules. The web application will expose AJAX services for authenticating and authorizing users through the provider interface (and via the database). The Windows application will use the web application for authentication and authorization.
Phase 1
For phase 1, we'll tackle the database. Identify or create a target database to store user and role information. Note that this will not work on user instance attached databases (like the kind you would put in your App_Data folder). You'll need to actually create a database on some instance of SQL Server somewhere. Either SQL 2000 or SQL 2005 will work, as far as I know.
Run C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regsql.exe to create SQLProvider tables, views and procs. Yes, this is in the v2 folder. You can launch it just by double-clicking the executable, which will launch a console window and then a small wizard GUI. Step through the wizard to create the objects in your target database. You can also use this utility to remove said tables, views and procs from a given database. Objects will have "aspnet" in their names. Once you've done that, try opening the database and checking out the number of tables, views and procs generated for you.
Phase 2
Now for phase 2, let's create a website we can use for authorization and authentication. Create a new website or web application project. Make sure it is "AJAX enabled." In Visual Studio 2008, this happens by default. You can actually delete Default.aspx, since this tutorial will not use it. If App_Data exists, you can delete that too. Open web.config and prepare to configure the membership provider. This requires four steps.
Add a connection string to the previously created and configured database. Note that the name of the connection string is important and will be used in the following step. For the example, I used aspnet_Regsql on a database called CASExample in SQL 2005. Then I detached it and copied the MDF file to the website App_Data folder. This allows me to distribute the code easily for tutorial purposes. Usually, I do not use the App_Data folder, but prefer database instances on servers. You can take whatever approach is the most comfortable for you. I also clear the connection strings collection with a clear tag because there is a connection string inherited from machine.config, which I dislike.
<connectionStrings>
-->
<add name="casExampleConnectionString"
connectionString=
"data source=.\SQLEXPRESS;Integrated Security=SSPI;" +
"AttachDBFilename=|DataDirectory|CASExample_data.mdf;User Instance=true"
providerName="System.Data.SqlClient"/>
-->
-->
</connectionStrings>
Then configure the application to use Forms authentication for collecting user login info from browser (this is under system.web). Note that this will set Forms authentication, but will not restrict access to the website or any of its materials. All this does is set up the website to use Forms authentication IF authentication is requested or used.
<authentication mode="Forms" />
Next, add a membership block to system.web to configure the application to use the SQL database for membership. These are the minimum settings you need to set in order to use membership. There are many others regarding password strength, etc. Note that there is a default membership provider defined in machine.config which will try to attach to a database called aspnetdb.mdf in your App_Data folder. This gets on my nerves, so I clear existing providers with a <clear />
element.
<membership defaultProvider="casExampleSqlProvider">
<providers>
<clear />
<add name="casExampleSqlProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="casExampleConnectionString"
applicationName="CASExample" />
</providers>
</membership>
MSDN has more information on membership providers. Now add a role manager block to system.web to configure the application to use the SQL database for roles. Like the membership block, there are other settings, but these are the minimum. The clear block also removes the machine.config defined provider.
<roleManager enabled="true" defaultProvider="casExampleSqlRoleProvider">
<providers>
<clear />
<add name="casExampleSqlRoleProvider"
connectionStringName="casExampleConnectionString"
applicationName="CASExample"
type="System.Web.Security.SqlRoleProvider" />
</providers>
</roleManager>
MSDN has more information on roleManager providers. At this point, the website is configured to use membership and roles. If you were interested in creating a "secured" website, you'd be ready to go at this point. However, a Windows application will not be able to utilize the membership and roles services directly. For that, you have to expose the services via AJAX. Thankfully, this is a snap. Add this to web.config (piece it together if you already have a system.web.extensions
section).
<system.web.extensions>
<scripting>
<webServices>
<authenticationService enabled="true" />
<roleService enabled="true"/>
</webServices>
</scripting>
</system.web.extensions>
MSDN has a full AJAX example where you can use JS client script in a web page to call the authentication services. A review of the example JavaScript on that page shows that the ASP.NET AJAX engine creates a Sys.Services.AuthenticationService
object for us automatically. We're interested in a Windows client application, so we will skip this example.
Now we at least need one user and one role in our system. The fastest way to create these is to use the ASP.NET Website Administration tool available through Visual Studio. This is definitely not the best way, nor the preferred way to manage users and roles, but it will work for a tutorial. In Visual Studio, make sure the website is selected and go to Website, ASP.NET Configuration. This will launch the ASP.NET config website. As a bonus, Visual Studio 2008 doesn't seem to mangle web.config like Visual Studio 2005 did.
Click the Security tab and create two roles, Admin and User. Also create two users, one in each role. We'll need these later for testing the Windows application. In my sample database, the users are:
- username:admin password:admin!!
- username:user password:user!!!
Phase 3
Moving on to phase 3, we create a Windows application. Create a Windows Forms application using default settings. To make it easy, add it to the same solution that the web application is in. Open the project properties window. Switch to the Services tab. Check the box for "Enable client application services." Make sure the radio button for "Forms authentication" is selected. Enter the base URL for the web application in your solution -- no page names. In my example, the URL is http://localhost:2539/Web. Also enter this in the box for Roles service location.
Let's find out what just happened under the hood. Open up app.config. Note that it has added a configuration section for client application services, with two nodes corresponding to membership and roles. Also note that these have URLs embedded in the settings, which would be important to change during deployment (hint hint). Also note that it has an appSettings
entry for the service URL, which is blank. This would be the entry for profile services if you were using them. Also note that these URLs have appended a "file name" to virtual files called Authentication_JSON_AppService.axd and Role_JSON_AppService.axd. Requests for these "pages" (and other *.axd files) get interpreted by the ASP.NET engine and routed to the appropriate HTTP handlers. This supports script files when needed, very much like other items in the AJAX toolkit and extenders. Apparently .NET creates these particular files when we add the correct nodes into web.config.
We have just turned magic into Microsoft generated files, if you didn't have enough of those lying around already. AJAX and custom HTTP handlers are beyond the scope of this tutorial, though, so feel free to close app.config at this point.
In the Windows application, add a reference to System.Web. Rename Form1
to LoginForm
. Add two text boxes, named txtUsername
and txtPassword
, and a button named btnLogin
. Make txtPassword
have a UseSystemPasswordChar
property valued true
. Make btnLogin
the AcceptButton
for the form.
Add a second form called MainForm
. Put a menu strip on it. Add two top-level menu items, one with text "Admin menu" and one with text "User menu." Give each of these top-level menu items a few subitems. Visual Studio should name the two top-level menu items adminMenuToolStripMenuItem
and userMenuToolStripMenuItem
. If not, just make a mental note of what their names are.
Open Program.cs. The last line in the main method should be Application.Run(new LoginForm());
. Replace that with this block:
ApplicationContext _applicationContext = new ApplicationContext();
LoginForm login = new LoginForm();
if (login.ShowDialog() == DialogResult.Yes)
{
MainForm m = new MainForm();
m.Show();
Application.DoEvents();
Application.Run(_applicationContext);
}
else
{
Application.Exit();
}
This will launch the login form when the application begins and will look for it to return a Yes
value for DialogResult
. If it does, the application will fire up the main form and pass control to it. Otherwise, the application exits. See The Code Project article Developing Next Generation Smart Clients using .NET 2.0 working with Existing .NET 1.1 SOA-based XML Web Services by Omar Al Zabir for more details on this approach. Go to the design view for LoginForm
. Double click btnLogin
and add the following code to its click event handler.
bool isValid =
System.Web.Security.Membership.ValidateUser(txtUsername.Text, txtPassword.Text);
if (isValid)
{
this.DialogResult = DialogResult.Yes;
}
else
{
MessageBox.Show("Login unsuccessful.");
this.DialogResult = DialogResult.None;
}
The first line uses the built-in ASP.NET membership to validate the user credentials. If it's successful, the form's DialogResult
is set to Yes
, which our Program.cs is expecting as a success value. Otherwise, a message is shown that the login was unsuccessful and DialogResult
is set to None
, which will cause Program.cs to exit. Obviously, this chunk of code needs error handling and it could be improved to allow users to attempt credentials more than once. Open up MainForm
in the designer. Add an event handler for the Closed
event and put this code in the handler:
Application.Exit();
The final step is to add some role handling to MainForm
. Create a Load
event handler and put this code in the handler:
adminMenuToolStripMenuItem.Visible =
System.Threading.Thread.CurrentPrincipal.IsInRole("Admin");
This will hide the admin menu control if the logged-on user is not in the Admin role. Obviously, this could be refactored and handled better, but it gets the point across.
Conclusion
We now have an end-to-end Windows client using ASP.NET membership and roles services. Set the client program as the start-up project in the solution and give it a run. You should be able to log in as a user from either role and, additionally, you should only see menu controls appropriate to your logon's role. See code samples for full example.
Helpful Links
History
- 7 December, 2007 -- Original version posted