Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 10

programmatically reverse engineer a regular expression would have made this helper method way too complex, and it is doubtful that you could even write to code to successfully accomplish this. It is up to the custom provider implementation whether or not it should even try to validate an auto-generated password against a specified regular expression — by way of comparison, neither the SQL nor AD-based ASP.NET provides attempt this.

Tracking Online Users

The Membership feature has the ability to keep track of users who are considered active on a website (that is, online) versus users who are in the system but have not necessarily been active within a configurable time period. The time period in which a user must be active, and thus considered online, is defined by the Membership.UserIsOnlineTimeWindow property. As discussed earlier, the internal implementation of MembershipUser.IsOnline uses this configuration property in conjunction with the user’s LastActivityDate to determine whether a user is considered online.

For this functionality to work, though, a custom provider must update the LastActivityDate inside of various methods. The MembershipProvider also exposes a method that can be used to get the count of online users for a website.

GetNumberOfUsersOnline — If a provider stores the LastActivityDate for its users, it should implement this method. The return value is a count of the number of users whose LastActivityDate is greater than or equal to the current date time less the UserIsOnlineTimeWindow. Note that an implementation of this method may result in a very expensive query or aggregation being performed. Although the ASP.NET SqlMemebershipProvider doesn’t do anything to mitigate this issue, custom providers may want to implement some kind of internal caching logic so that calls to the GetNumberOfUsersOnline method do not trigger incessant table scans or other expensive operations in the underlying data store. If a provider does not support keeping track of when users are online, it can instead throw a NotSupportedException from this method.

ValidateUser — Each time a user attempts to login, the LastActivityDate should be updated. There is no strict rule on whether this date should only be updated for successful logins, or for both successful and failed logins. The SqlMembershipProvider happens to update the date for both cases, but it is also reasonable to say a user isn’t truly online until after a successful login has occurred.

GetUser — Both GetUser overloads have a parameter called userIsOnline. If the provider supports updating a user’s LastActivityDate, and if this parameter is set to true, then each time a user object is retrieved it should first have its LastActivityDate updated. Providers that don’t support counting online users can just ignore the userIsOnline parameter. It also would not be unreasonable for a custom provider to throw a NotSupportedException if userIsOnline is set to true and the provider doesn’t support tracking online users,

CreateUser — Custom providers can choose to set the LastActivityDate to the creation date (SqlMembershipProvider does this) or instead set LastActivityDate to a default value. It is up to you to determine if it makes more sense to say that a newly created user is immediately online or not. Some developers will probably prefer to not have CreateUser mark a MembershipUser as online if users are usually created in a batch process of if user accounts are created by someone other than a live user on a website.

UpdateUser — A provider can support updating a user’s LastActivityDate using the value on the MembershipUser object passed to this method.

392

Membership

In the SqlMembershipProvider there aren’t any other Membership operations that result in updating a user’s LastActivityDate. Other methods that update a user’s password or password question and answer do not cause any changes to LastActivityDate when using the SQL provider. Again, though, this is a philosophical decision that can be argued either way. There would be nothing wrong with a custom provider when you feel that these types of operations should result in an update to

LastActivityDate.

General Error Handling Approaches

If you look closely at the MembershipProvider definition, you can see that there is one method with an out parameter (the status parameter on CreateUser), whereas all of the other methods just handle input parameters. Furthermore, the default providers typically have different error behavior depending on whether a Boolean is used as a return value. Unfortunately, there wasn’t enough time in the ASP.NET 2.0 development cycle to fine-tune error handling and exception behavior for the Membership feature, so the end result can be a bit confusing at times and less than elegant.

The general rules of thumb are listed here. Both the SQLand AD-based providers follow these rules:

For all methods, if the provider is asked to do something that it doesn’t support, it should just throw a NotSupportedException. This can be the case when an entire method is simply not supported. This can also occur if a method is implemented, but another configuration setting on the provider indicates that the method should not succeed. For example, the default providers implement ResetPassword, but if EnablePasswordReset is set to false in configuration, then the providers throw a NotSupportedException. Another example is when a parameter to a method was supplied (for example, providerUserKey for CreateUser) but the provider cannot actually do anything with the parameter.

If a method has an out parameter for communicating a result status, the method should usually return error conditions via that parameter.

A well-written provider should perform a rigorous set of parameter validations that ensures method parameters have reasonable values. The ASP.NET providers throw an Argument Exception for parameter validations that fail for non-null values, and they throw an Argument NullException for parameter validations that fail because of unexpected null values.

If the return type of a provider method is Boolean, and if the success of the method

depends on a correct password being passed to the method, the method should simply return false for bad passwords. This means methods like ValidateUser, ChangePassword, and ChangePasswordQuestionAndAnswer should simply return false if the provider determines that the user either supplied the wrong password or if the user was already locked out or not approved. The theory here is that especially for a method like ValidateUser, it makes more sense to provide a “thumbs-up/thumbs-down” result than to throw an exception for a bad password.

For the other methods that return a Boolean value (DeleteUser and UnlockUser), the provider can return a value of false if the operation failed because the user record couldn’t be found. As you will see shortly, in other methods a nonexistent user record instead causes an exception with the default ASP.NET providers. Although no Login controls depend on these two methods currently, it is possible that future Login controls might use these methods, in which case the controls would expect custom providers to follow the same behavior.

393

Chapter 10

A provider should throw the special MembershipPasswordException type when a bad password answer is supplied to either ResetPassword or GetPassword. This type allows developers and the Login controls to recognize that the specific problem is an incorrect password answer. Unfortunately, this behavior is a perfect example of the somewhat schizophrenic exception and error-handling behavior in the default providers; it would have been better to rationalize the behavior of bad passwords and bad password answers in a more consistent manner.

If a provider performs business-logic related checks in the provider or in the back-end data store, it can use the ProviderException class to return back the error condition. The kinds of checks that can fail include not finding the specified user in the system (for example, you attempt to update a nonexistent user) or attempting to use a mal-formed regular expression for password validations. This was the approach used by the ASP.NET providers to eliminate the need to spam the System.Web.Security namespace with many custom exceptions. However, it is also a reasonable approach for building a rich exception hierarchy that is more expressive and return. If you intend for a custom provider to work with the various Login controls though, your custom exceptions should derive from ProviderException. The Login controls will, in many cases, suppress exceptions in order to perform failure actions or to display failure text configured for a control. The Login controls can only do this though for exception types that they recognize, ProviderExceptions and ArgumentExceptions being two of the exception types that they handle.

Last, the default ASP.NET providers usually don’t handle unexpected exceptions that can arise from the underlying classes they call into. For example, the SqlMembershipProvider doesn’t catch and remap SQL Server related exceptions. The ActiveDirectoryMembershipProvider for the most part also doesn’t suppress or remap exceptions from the System

.DirectoryServices namespace. The assumption is that data-layer exceptions are usually indicative that something has seriously gone wrong, and as a result these types of exceptions are not error conditions that the provider knows how to handle.

The “Primary Key” for Membership

I have alluded to the fact that the Membership feature considers a username to be part of the “primary key” for the Membership feature. Because the feature is provider-based, and all of the ASP.NET 2.0 SQL providers support an “applicationName” attribute in configuration, the precise statement is that the Membership feature implicitly considers the combination of applicationName and username to be an immutable identifier for users. Although a more database-centric definition of a primary key could have been modeled in Membership and other related features, the intent was to keep the user identifier as simple and as generic as possible.

Because it is likely that just about any conceivable Membership store ever devised will support a string type, choosing username and application name seems pretty safe. This also means that it is possible for developers to write custom features that link to Membership data at an object level in a reliable manner. For example, if you had an inventory application running off in a corner somewhere that you needed to integrate with a website running Membership, it is pretty likely that you will at least be able to find a string-based username in the inventory system that has some mapping and relevance to your website. Using a database primary key/foreign key relationship probably won’t work if your inventory system is running on some “interesting” relic that has been repeatedly upgraded over the decades, other systems that you need to integrate with are black boxes and you can’t just dive down and set up relationships at the data layer.

394

Membership

In other words, username and application name were chosen as the “primary key” because you can always pass these values around in a middle-tier object layer without requiring any kind of compatibility between features lower down in the data layer. In some cases, though there may not be a concept of an application name for some data stores. The ActiveDirectoryMembershipProvider for example doesn’t do anything with the applicationName attribute in configuration, whereas the SqlMembershipProvider does use the application name to create part of the primary key and actually stores the application name in the database.

However, even in the case of the AD-based provider you could argue that each separate instance of an AD provider defined in configuration logically correlates to an “application.” So, if you wanted to use Web Parts Personalization (using the SQL provider) with the AD membership provider, you could still separate user data in the Web Parts Personalization data store based on which AD provider was actually used to authenticated a user. It would be up to you to set up the applicationName attribute for your Web Parts Personalization providers in a way that correlated to the different configured AD membership providers, but you could do this pretty easily.

Although having a common identifier for objects is useful, it doesn’t perform well. If you know that you have features that are compatible at the data layer with Membership (for example, maybe you have all of the tables for your feature and the Membership feature in the same database), it is probably easier and more natural to pass around database primary keys (for example, GUIDs, integers, and the like). There is an even bigger issue if you allow changes to usernames. Although the Membership API doesn’t support this, and none of the other provider-based features support it, it is a common request by developers to have the ability to change usernames after a user has been created. Because all of the ASP.NET features key off username, this can be a bit awkward; from a data integrity standpoint primary keys really aren’t supposed to be updated.

The way most developers deal with this design problem is to create a data-store-specific primary key value, and then to mark the username as some type of alternate key. The alternate key ensures uniqueness, while the primary key ensures that data relationships aren’t mucked up each time someone updates a username. Of course, you may already be thinking what about that ProviderUserKey property we just saw a while back? That property (and it also shows up as a parameter in a few places in Membership) was the start of an abortive attempt to provide a more data-layer centric approach to handling Membership data. However, further integration of this property into the Membership feature and other provider-based features was halted due to time constraints.

If you don’t care about the portability of the username and application name, you can create and retrieve users based on the ProviderUserKey. The reason for the name of this property on MembershipUser is to make it clear that not all providers are necessarily databases. So, rather than calling the property PrimaryKey, the more generic name of ProviderUserKey was chosen.

The CreateUser method lets you pass in an explicit value for the database primary key, assuming that the underlying provider allows you to specify the primary key. The GetUser method has an overload that allows you to retrieve a user based on the data store’s primary key value. Of course, this probably strikes you as a rather limited offering: What about updating a user based on the ProviderUserKey? Well you can’t do that. For that matter, other than creating a user and getting a single user instance back, there is no other support in the Membership feature, or any other feature, for manipulating data based on the data-store-specific primary key. There may (or may not be) work in a future release to bake the concept of a primary key more deeply into the Membership feature as well as the related Profile, Role Manager, and Web Parts Personalization features.

395

Chapter 10

One very important thing to keep in mind though with data-store-specific keys is that after you start designing provider-based features with a hard dependency on a specific key format, you have potentially limited your interoperability with other features, including features that no one has dreamed up yet. Although the combination of username and application name can be a bit awkward at times, it does it make it possible for completely random features to integrate at the level of the various provider-based object APIs.

For example, although Role Manager is frequently referred to as a companion feature to Membership, the reality is that you don’t need to use Membership to leverage Role Manager. You can use Role Manager on an intranet web server with Windows authentication. Because Role Manager keys off of username and application name, it is very easy to use the domain credentials of the user as the username value in Role Manager even though no data-layer relationship exists between Role Manager and an Active Directory environment. The application name in Role Manager can then be set based on the name of the website that is using the feature, or it can be set based on the AD domain that users authenticate against prior to using the application.

Supported Environments

Although the Membership feature is technically a part of ASP.NET 2.0 (the feature exists in the System

.Web.Security namespace and is physically located in System.Web.dll), you can use the Membership feature outside of ASP.NET. This means that you can call any of the functionality in the Membership feature from console applications, NT service applications, fat client applications (that is, Winforms apps), and so on. Although you will need to reference the appropriate ASP.NET namespace and assembly, beyond this requirement nothing special is needed to get Membership working outside of ASP.NET.

The Membership feature always requires at least Low trust to work. For ASP.NET applications, this means that you must run in Low trust or higher. For a non-ASP.NET application, the AspNetHostingPermission must be granted to the calling code with a level or Low or higher.

As an example of using the feature outside of ASP.NET, you can write a basic console application that creates MembershipUser instances. This can come in handy if you need to prepopulate the database for the SqlMembershipProvider. When you create a non-ASP.NET application, it must reference System.Web.dll. Figure 10-1 shows the proper reference for a console application set up in Visual Studio 2005.

Because the Membership feature has default settings defined in machine.config, you don’t necessarily need to configure the feature for your applications. However, the default applicationName as set in configuration is /. This value probably won’t make much sense for complex applications, so you may need to change it for both your web and non-web applications. Additionally, the default Membership provider in machine.config points at a local SQL Server Express database, which is probably not useful for a lot of corporate applications.

396

Membership

Figure 10-1

In non-ASP.NET applications, you can add an app.config file to the project that contains the desired <membership /> configuration section. One thing to note is that if you add app.config to a nonASP.NET project, it is created without the namespace definition on the <configuration /> element. This has the effect of disabling IntelliSense within the design environment. Don’t worry though because the configuration syntax is the same regardless of whether you are working with an ASP.NET application or a non-ASP.NET application.

The app.config file for the sample console application is shown here with the type of the provider snipped for brevity. The connection string shown below also assumes that you have already set up the aspnetdb database in SQL Server using the aspnet_regsql tool:

<configuration>

<connectionStrings>

<add name=”ConsoleDatabase”

connectionString=”server=.;Integrated Security=true;database=aspnetdb” /> </connectionStrings>

<system.web>

<membership defaultProvider=”ConsoleMembershipProvider”> <providers>

<clear />

<add name=”ConsoleMembershipProvider” type=”System.Web.Security.SqlMembershipProvider, System.Web...” connectionStringName=”ConsoleDatabase” applicationName=”MyConsoleApplication” />

</providers>

</membership>

</system.web>

</configuration>

Even though it may look a little strange, it is perfectly acceptable to have a <system.web /> configuration section located inside of a configuration file for a non-ASP.NET application. From the Framework’s point of view, <system.web /> and its nested configuration sections are just another set of information to parse. There is no dependency on an ASP.NET application host for the Membership-related configuration classes.

397

Chapter 10

The previous sample configuration clears the <providers /> collections. It is usually a good idea to clear out provider collections if you don’t need any of the inherited definitions. In the case of the sample console application, you need your own definition to set the applicationName attribute appropriately. As a result, there is no reason to incur the overhead of instantiating the default provider defined up

in machine.config. Also notice that the configuration file resets the defaultProvider on the <membership /> element to point at the ConsoleMembershipProvider definition.

At this point, you have done everything necessary from a configuration perspective to get the console application to work with the Membership feature. The only thing left to do is to write some code.

using System;

using System.Web.Security;

namespace MemConsoleApp

{

class Program

{

static void Main(string[] args)

{

MembershipCreateStatus status; MembershipUser mu =

Membership.CreateUser(args[0], args[1], args[2],

args[3], args[4], true, out status);

Console.WriteLine(status.ToString());

}

}

}

The sample application uses the static Membership class to create a user. To reference the feature, it includes a namespace reference at the top of the file to System.Web.Security. It expects the commandline parameters to be the username, password, email address, password question, and password answers respectively. For brevity, the application doesn’t include any error checking on the arguments. You can see how little code is necessary to take advantage of the Membership feature; it probably takes more time to set the assembly reference and tweak the configuration file that it does to write the actual code that creates users.

After compiling the application you can invoke it from the command line, and the results of the user creation will be output to the console. A successful user creation looks like this:

MemConsoleApp.exe testuser pass!word test@nowhere.org Question Answer

Success

Because the console application uses the CreateUser overload that returns a status, if you attempt to create the same user a second time, you see the following error message.

MemConsoleApp.exe testuser pass!word test@nowhere.org Question Answer

DuplicateUserName

398

Membership

In this case, the error message is just the string version of the returned MembershipCreateStatus. Although the sample application only shows user creation, the full spectrum of the Membership feature is available for you to use outside of ASP.NET. You can consume the existing API as well as write custom providers for use in non-web environments. In future releases, Membership may also be extended further so that features such as Web Service–callable providers will be available right out of the box.

Using Custom Hash Algorithms

The <membership /> configuration element includes the hashAlgorithmType configuration attribute. By default the Membership feature (or more specifically the SqlMembershipProvider) uses SHA1 when storing passwords. You can set this attribute to any string that the .NET Framework recognizes as a valid hashing algorithm, and the SqlMembershipProvider will use that algorithm instead. If you look at the documentation for the System.Security,Cryptography.HashAlgorithm class’s Create method, there is a list of the default strings (that is, simple names) that the .NET Framework recognizes and supports for referring to hash algorithms. Any one of these strings can be used in the hash AlgorithmType attribute. You can retrieve the name of the hashing algorithm configured for the Membership feature by getting the value of the Membership.HashAlgorithm property.

Although the hash algorithm is a feature-level setting, it is really more of an opt-in approach for individual providers. The setting on the <membership /> element would be useless if individual Membership providers didn’t explicitly read the value from the Membership.HashAlgorithm property and then internally make use of the correct algorithm. Currently, the hashing functionality for the SqlMembership Provider calls an internal method on MembershipProvider. This internal method, in turn, creates the appropriate hash algorithm based on the hashAlgoriothmType attribute and then hashes the password with a random salt value. In a future release, the internal method that does this may be made public. For now, though, this means custom provider implementers that support password hashing need to write code that follows the same approach:

1.Fetch the value of Membership.HashAlgorithm.

2.Call HashAlgorithm.Create, passing it the string from step 1.

3.With the resulting reference to the hash algorithm class, hash the password and optionally other information such as a random password salt if the provider supports this.

4.Store the hashed value in the back-end data store

Assuming that you can depend on providers to follow these steps, you have the ability to influence a provider’s hashing processing by configuring different hash algorithms. Using any of the default hash algorithms in the Framework is very easy; you just set the hashAlgorithmType attribute to something else such as SHA256, SHA512 and so on.

What happens though if you need to configure a hash algorithm that doesn’t ship in the Framework? In this case, you have the option of writing your own hash algorithm implementation and registering it with the .NET Framework. Although you can definitely create your own custom hashing algorithm that you instantiate and call directly from inside of a web page, because Membership depends on the loosely typed HashAlgorithm.Create method, you must register your hash algorithm with the .NET Framework for it to be used by the SqlMembershipProvider or any other providers that follow the same programming approach.

399

Chapter 10

To see how this works, you can create a basic hash algorithm class like the one shown here:

using System.Security.Cryptography; using System.Text;

namespace CustomHashAlgorithm

{

public class DummyHashClass : HashAlgorithm

{

protected override void HashCore(byte[] array, int ibStart, int cbSize)

{

return; }

protected override byte[] HashFinal()

{

return Encoding.UTF8.GetBytes(“DUMMYHASHVALUE”); }

public override void Initialize()

{

return; }

}

}

Clearly, you would never use an “algorithm” like this in production, but for showing the hashAlgorithmType attribute in configuration, it is good enough. Rather than actually hashing anything, the custom class always returns a hard-coded string. After you compile this class and deploy the assembly into the /bin folder of an ASP.NET application, the next step is to make the class visible to the cryptographic infrastructure in the .NET Framework.

You register custom cryptographic algorithms, both hashing and encryption algorithms, using the

<crytpographySettings /> configuration element found within <mscorlib />.

<mscorlib>

<cryptographySettings>

<cryptoNameMapping>

<cryptoClasses>

<cryptoClass

MyDummyHashClass=”CustomHashAlgorithm.DummyHashClass, CustomHashAlgorithm”/> </cryptoClasses>

<nameEntry name=”TestAlgorithm” class=”MyDummyHashClass”/>

</cryptoNameMapping>

</cryptographySettings>

</mscorlib>

The way this configuration works is:

The <cryptoClass /> element associates a name (in this case MyDummyHashClass) with a

.NET Framework type. In this case, I am using a reference to just a class and an assembly. In production applications, your custom hash algorithm type would probably be in the GAC and, thus, you would instead use a strong named reference here. Because the sample is not strong named, the assembly CustomHashAlgorithm has to be deployed in an ASP.NET application’s /bin directory for the type to be loaded.

400

Membership

The <nameEntry /> element associates a friendly name with the custom hash algorithm class. In the sample configuration, this allows TestAlgorithm to be passed to HashAlgorithm

.Create, which will then return a reference to the DummyHashClass type.

A very important note about this configuration: You must place the configuration in machine.config! If you try to place the configuration section inside of web.config, the cryptography infrastructure will never see your custom type because the <mscorlib /> cryptography settings are only valid when defined in machine.config. Although you can place them in other configuration files, they will never be processed. If you end up banging your head against a wall wondering why your custom hash class is never being used, it is probably because the configuration for it is not in the right place.

With the sample hash algorithm configured in machine.config, you can create a sample ASP.NET application that makes use of it. The following configuration element tells the Membership feature to use the custom type.

<membership hashAlgorithmType=”TestAlgorithm” />

Now if you create a new user with the SqlMembershipProvider, the new user’s password will be hashed using the custom hash algorithm. You can verify this by looking in the database — you will see that the password value is RFVNTVlIQVNIVkFMVUU=. This is just the base64-encoded representation of the byte[] returned by any hash algorithm. If you run the following code snippet to decode this string, the Membership feature successfully use the custom hash algorithm and end up with a password of

DUMMYHASHVALUE.

byte[] dbResult = Convert.FromBase64String(“RFVNTVlIQVNIVkFMVUU=”); string dbString = Encoding.UTF8.GetString(dbResult); Response.Write(“The encoded password is “ + dbString);

Because the registration of custom hash algorithms has to occur in machine.config, you will probably find custom hash algorithms (that is, non-Framework algorithms) primarily useful when they need to be used globally for many applications on a server. Although it is possible, it probably doesn’t make much sense to use Membership in a way where custom hash algorithms are defined on a per-application basis — that is, dozens of applications on a machine with each application using a completely different custom hashing implementation. This kind of approach would result in dozens of custom algorithms needing to be registered up in machine.config.

Summar y

For a lot of developers, the Membership feature will be equivalent to using the Login controls and the public static Membership class. If you never have to deal with multiple providers, or provider-specific functionality, everything you need to use can be found on the Membership class. However, more complex sites will probably need to code against the MembershipProvider class — especially if they need to handle multiple providers.

Because the Membership feature deals with various aspects of a user, the MembershipUser class is available for carrying out user-oriented functions such as password management and user updates. As with the MembershipProvider class, you can also choose to implement a custom MembershipUser class. The usual coding approach is for custom provider implementers to optionally supply a custom

MembershipUser class as well.

401