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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 10

ProviderName — The string name of the provider that manages the MembershipUser instance. Because the MembershipUser class supports a number of methods that deal with the user object, each user object needs to know the provider that should be called. In other words, a MembershipUser’s methods act as a mini-façade on top of the provider that initially was responsible for creating the MembershipUser. As a side note, the reason that this property is a string (it was a MembershipProvider reference early on in ASP.NET 2.0) is to make it possible to serialize a MembershipUser instance. If this property had been left as a reference type, this would have required all MembershipProvider instances to also be serializable.

Email — An optional email address for the user. This property is very important if you want to support self-service password resets because without an email address there is no way to communicate to the users the newly generated password or their old password.

IsApproved — A Boolean property that provides a basic mechanism for indicating whether a user is actually allowed to login to a site. If you set IsApproved to false for a user, even if the user supplies the correct username-password credentials at login, the login attempt (that is, the call to ValidateUser) will fail. With the IsApproved property, you can implement a basic twostep user creation process where external customers request an account and internal personnel approve each account. The Web Administration Tool that is accessible inside of the Visual Studio environment provides a UI for this type of basic two-step creation process.

IsOnline — A Boolean property indicating whether the user has been active on the site with the last Membership.UserIsOnlineTimeWindow minutes. The actual computation of whether a user is considered online is made inside of this property by comparing the LastActivityDate property for the user to the current UTC time on the web server. If you rely on this property, make sure that your web servers regularly synchronize their time with a common time source. Note that the IsOnline property is not virtual in this release, so if you want to implement alternate logic for IsOnline you have to add your own custom property to a derived implementation of MembershipUser.

IsLockedOut — A Boolean property indicating whether the user account has been locked out due to a security violation. This property has a distinctly different connotation from the IsApproved property. While IsApproved simply indicates whether a user should be allowed to login to a site, IsLockedOut indicates whether an excessive number of bad login attempts have occurred. If you support self-service password reset or password retrieval using a password question-and-answer challenge, this property also indicates whether an excessive number of failed attempts were made to answer the user’s password question.

PasswordQuestion — You can choose to support self-service password resets or self-service password retrieval on your site. For added protection, you can require that the user answer a password question before resetting the password or retrieving the current password. This property contains the password question that was set for the user. It is up to you whether to allow each user to type in a unique question, or if you provide a canned list of password questions. Note that even though you can retrieve the password question for a user, the password answer is not exposed as a property because it should only be managed internally by providers.

LastActivityDate — The last date and time that the user was considered to be active. Certain methods defined on MembershipProvider are expected to update this value in the back-end data store when called. Other companion features, such as Profile, and Web Parts Personalization, update this value assuming that you use the ASP.NET SQL providers for all of these features. The property is returned as a local date time, but providers should internally store the value in UTC time.

372

Membership

LastLoginDate — The last date and time a successful call to ValidateUser occurred. Providers are expected to update this value after each successful password validation. The property is returned as a local date time, but providers should internally store the value in UTC time.

LastPasswordChangedDate — The last date and time that the password was changed — either by the user explicitly updating their password or by having the system create a new auto-gener- ated password. The property is returned as a local date time, but providers should internally store the value in UTC time.

LastLockoutDate — The last date and time that the user account was locked out — either due to an excessive number of bad passwords or because too many bad password answers were supplied. This value is only expected to be reliable when the account is in a locked out state (that is, this.IsLockedOut is true). For accounts that are not locked out, this property may instead return a default value. The property is returned as a local date time, but providers should internally store the value in UTC time.

Extending MembershipUser

The MembershipUser class is public but it is not sealed, so you can write derived versions of this class. Most of its public properties are defined virtual for this reason. In fact, the ActiveDirectoryMembershipProvider takes advantage of this and uses a derived version of MembershipUser to help optimize the interaction of the provider with an Active Directory or Active Directory Application Mode data store.

The class definition for MembershipUser is:

public class MembershipUser

{

//Virtual properties

public virtual string UserName{ get; } public virtual object ProviderUserKey{ get; } public virtual string Email{ get; set; }

public virtual string PasswordQuestion{ get; } public virtual string Comment{ get; set; } public virtual bool IsApproved{ get; set; } public virtual bool IsLockedOut{ get; }

public virtual DateTime LastLockoutDate{ get; } public virtual DateTime CreationDate { get; } public virtual DateTime LastLoginDate { get; set; }

public virtual DateTime LastActivityDate { get; set; } public virtual DateTime LastPasswordChangedDate { get; } public override string ToString();

public virtual string ProviderName { get; }

//Non-virtual properties public bool IsOnline { get; }

//Constructors

 

public MembershipUser(

 

string

providerName,

string

name,

object

providerUserKey,

string

email,

373

Chapter 10

string

passwordQuestion,

string

comment,

bool

isApproved,

bool

isLockedOut,

DateTime

creationDate,

DateTime

lastLoginDate,

DateTime

lastActivityDate,

DateTime

lastPasswordChangedDate,

DateTime

lastLockoutDate )

protected MembershipUser() { }

//Methods - all are virtual

public virtual string GetPassword()

public virtual string GetPassword(string passwordAnswer)

public virtual bool ChangePassword(string oldPassword, string newPassword) public virtual bool ChangePasswordQuestionAndAnswer(

string password, string newPasswordQuestion, string newPasswordAnswer) public virtual string ResetPassword(string passwordAnswer)

public virtual string ResetPassword() public virtual bool UnlockUser()

}

As mentioned earlier, the IsOnline property cannot be overridden, so you are left with the default implementation. All of the other properties though can be overridden. The default implementation for these properties simply returns the property values that were set when the object was first constructed. As you can see from the lengthy constructor parameter list, the usage model for MembershipUser is:

1.Either a provider or your code new()’s up an instance, passing in all of the relevant data.

2.You subsequently access the properties set in the constructor via the public properties.

3.If you want to then update the MembershipUser object, you pass the modified instance back to the UpdateUser method implemented either on the static Membership class or on a specific

MembershipProvider.

Note that with this approach updating the user is a little awkward because there is no update method on the user object itself. Instead, the user object is passed as a piece of state to the UpdateUser method on a provider.

The capability to override individual properties is somewhat limited though because you don’t have access to the private variables that back each of these properties. The most likely purpose of an override would be to throw an exception (for example, NotSupportedException) for properties that may not be supported by custom providers. For example, if you authored a custom provider that did not support the concept of account lockouts, you could throw a NotSupportedException from a

LastLockoutDate override.

All of the public methods currently defined on MembershipUser can be overridden. The default implementations of these methods are just facades that do two things:

Get a reference to the MembershipProvider based on the providerName parameter supplied in the constructor.

Calls the method on the MembershipProvider reference that corresponds to the public method on the MembershipUser object — for example the ResetPassword overloads on MembershipUser call the ResetPassword method on the appropriate provider.

374

Membership

The providerName parameter on the constructor is actually a very important piece of information that effectively limits any kind of “tricks” involving manual creation of providers. Remember from Chapter 9 that the provider initialization sequence is something that you can accomplish with a few lines of your own custom code.

However, if you attempt to instantiate MembershipProviders with your own code, and if you need to manipulate MembershipUser instances, your code will fail. Inside of the MembershipUser constructor a validation check ensures providerName actually exists in the Membership.Providers collection. If the provider cannot be found, an exception is thrown. If you wanted to try something like spinning up dozens or hundreds of provider instances on the fly without first defining the providers in configuration, the basic approach or just instantiating providers manually won’t work.

MembershipUser State after Updates

If you call any of the public methods on MembershipUser that affect the state of the user object (that is, all methods except for the GetPassword overloads), then the MembershipUser instance calls an internal method called UpdateSelf. Unfortunately in ASP.NET 2.0 this method is not public or protected, let alone being defined as virtual, so the behavior of this method is a black box. What happens is that after the state of the MembershipUser instance is modified, the base class internally triggers a call to GetUser() on the user object’s associated provider instance. If you look at a SQL trace on the SqlMembershipProvider, or if you trace method calls on a custom provider, this is why you always see an extra user retrieval running after most of the methods on MembershipUser are called.

With the MembershipUser instance returned from the GetUser call, the internal UpdateSelf method transfers the latest property values from the returned MembershipUser instance to the properties on the original MembershipUser instance. The idea here is that some of the public methods on MembershipUser cause changes to related properties — for example, calling ResetPassword implicitly changes the LastPasswordChangedDate. The theory was that it wouldn’t make sense for a method call to change the state of the MembershipUser instance and then have the instance not reflect the changes. Though arguably there isn’t anything wrong with a different approach that would have left the original MembershipUser instance intact despite the changes in the data store. Some developers will probably find it a little odd that the original MembershipUser instance suddenly changes on them.

Because some of the properties on a MembershipUser instance are public read-only properties, the behavior of this self-updating gets a little weird. The UpdateSelf method transfers updated values for read-only properties directly to the private variables of the MembershipUser base class. For properties that have setters, UpdateSelf transfers property data by calling the public MembershipUser setters instead. This means that if you have written a derived MembershipUser class, and overridden the public setters and the constructors, the UpdateSelf behavior may either bypass your custom logic or it may call your logic too many times.

For example, if a derived MembershipUser class overrides the constructor and performs some manipulations on PasswordQuestion prior to calling the base constructor, then the private variable holding the password question will reflect this work. If you then subsequently call ChangePasswordQuestionAndAnswer on the MembershipUser instance, the internal UpdateSelf method will cause the following to occur:

375

Chapter 10

1. A new MembershipUser instance is retrieved from the call to GetUser (assume that you write a custom provider that returns a derived MembershipUser instance). As a result, this new instance will have its password question processed in your custom constructor.

2. UpdateSelf then takes the result of MembershipUser.PasswordQuestion and transfers its value directly to the private variable on the original MembershipUser instance that stores the question.

With this sequence you are probably OK because the custom processing in your constructor happened only once and then the result was directly stored in a private variable on the original instance. What happens though for a property with a public setter — for example the Comment property? Now the sequence of steps is:

1.A new MembershipUser instance is retrieved from the call to GetUser. The new instance does something to the Comment in your custom constructor.

2.UpdateSelf takes the result of MembershipUser.Comment and calls the public Comment setter on the original MembershipUser instance. If you have custom logic in your setter as well, then it will end up manipulating the Comment property a second time, which will potentially result in a bogus value.

To demonstrate this, start out with a custom MembershipUser type, as shown below:

using System.Web.Security;

...

public class CustomMembershipUser : MembershipUser

{

public CustomMembershipUser() {}

//Copy constructor

public CustomMembershipUser(MembershipUser mu) : base(mu.ProviderName, mu.UserName, mu.ProviderUserKey, mu.Email,

mu.PasswordQuestion, mu.Comment, mu.IsApproved, mu.IsLockedOut, mu.CreationDate, mu.LastLoginDate, mu.LastActivityDate, mu.LastPasswordChangedDate, mu.LastLockoutDate) { }

public override string Comment

{

get

{ return base.Comment; } set

{

base.Comment =

value + “ Whoops! Extra modification occurred in property setter”;

}

}

}

Try using this custom type to retrieve a MembershipUser and perform what should be a no-op update:

...

MembershipUser mu = Membership.GetUser(“testuser”);

//Convert the MembershipUser into the custom user type

376

Membership

CustomMembershipUser cu = new CustomMembershipUser(mu);

Response.Write(“Comment before update: “ + cu.Comment + “<br/>”);

Membership.UpdateUser(cu);

Response.Write(“Comment after update: “ + cu.Comment);

When you run this code snippet in a page load event, the output is bit surprising:

Comment before update: This is the original comment

Comment after update: This is the original comment Whoops! Extra modification occurred in property setter

Even though the code snippet appears to change none of the properties on the MembershipUser instance, after the update the Comment property has clearly been modified. This is due to the behavior of the internal UpdateSelf method on MembershipUser — in this case, UpdateSelf was triggered by code inside of the Membership class implementation of UpdateUser. (Membership.UpdateUser calls an internal method on MembershipUser which in turn calls UpdateSelf). You will see the same side effect from calling methods on MembershipUser as well. If you run into this problem, you can avoid the “stealth” update by calling UpdateUser on a provider directly. Doing so bypasses the refresh logic hidden inside of the Membership and MembershipUser classes.

It is likely though that derived versions of MembershipUser probably won’t be changing the data that is returned inside of property setters. However, developers may author derived classes that implement custom dirty detection (that is, if the setters weren’t called and an update is attempted, do nothing with the MembershipUser object) as well as throw exceptions from unsupported properties.

For the case of dirty detection, the only real workaround is to override the methods as well as the properties on MembershipUser. Then you can write code in the method overrides that does something like:

using System.Web.Security;

public class CustomMembershipUser : MembershipUser

{

//Used by a custom provider to determine if the user object really //needs to be updated.

internal bool isDirty = false;

...

public override string Comment

{

set

{

base.Comment = value; isDirty = true;

}

}

public override bool ChangePassword(string oldPassword, string newPassword)

{

//When this call returns, UpdateSelf will have triggered the object’s //dirty flag by accident.

377

Chapter 10

bool retVal = base.ChangePassword(oldPassword, newPassword);

//reset your private dirty tracking flags to false at this point

isDirty = false;

}

}

On one hand, basically you need to explicitly manage your dirty detection logic and ensure that after you call the base implementation, your reset your internal dirty detection flags because they may have been spuriously tripped due to the way UpdateSelf works.

On the other hand, if you throw exceptions from some of your property getters and setters, you may be wondering if it is even possible to write a derived MembershipUser class. Theoretically, if the second the internal UpdateSelf method attempts to transfer property data back to the original MembershipUser instance, your custom class should blow up. In the finest programming tradition (and trust me — I mean this tongue in cheek), the solution in ASP.NET 2.0 is that the transfer logic inside of UpdateSelf is wrapped in a series of try-catch blocks. So, the guts of this method look something like:

try

{

Comment = newUserFromGetUser.Comment;

}

catch (NotSupportedException) { }

And here you thought jokes about Microsoft code relying on swallowing exceptions was a joke — however, ildasm.exe does not lie. Seriously though, the trick to making sure that a derived MembershipUser class doesn’t fail because of unimplemented properties is to always throw a NotSupportedException (or a derived version of this exception) from any properties that you don’t want to support. The internal UpdateSelf will always eat a NotSupportedException when it is transferring property data between MembershipUser instances. If you use a different exception type though, then you will quickly see that your derived MembershipUser type fails whenever its public set methods are called. Needless to day, making UpdateSelf protected virtual is on the list of enhancements for a future release!

The way in which updated property data is transferred back to the original MembershipUser instance is summarized in the following table:

Property Name

Transferred to Private Variable

Transferred Using Public Setter

 

 

 

Comment

No

Yes

CreationDate

Yes

No

Email

No

Yes

IsApproved

No

Yes

IsLockedOut

Yes

No

LastActivityDate

No

Yes

LastLockoutDate

Yes

No

LastLoginDate

No

Yes

 

 

 

378

 

 

 

Membership

 

 

 

 

 

Property Name

Transferred to Private Variable

Transferred Using Public Setter

 

 

 

 

 

LastPasswordChangedDate

Yes

No

 

PasswordQuestion

Yes

No

 

ProviderUserKey

Yes

No

 

 

 

 

Why Are Only Certain Properties Updatable?

Only a subset of the properties on a MembershipUser instance has public setters. The reasons for this differ depending on the specific property. The different reasons for each read-only property are described in the following list:

UserName — In this release of the Membership feature, a username is considered part of the primary key for a MembershipUser. As a result, there is no built-in support for updating the username. There are no public APIs in any of the application services that allow you to make this change, though of course there is nothing stopping enterprising developers from tweaking things down in the data layer to make this work. From an API perspective, because username is not meant to be updated, this property is left as a read-only property.

ProviderUserKey — Because this property is a data-store specific surrogate for UserName, the same feature restriction applies. The Membership feature doesn’t expect the underlying primary key for a user to be updatable. Again this may change in a future release.

PasswordQuestion — This piece of user data is updatable, but you need to use the ChangePasswordQuestionAndAnswer method to effect a change. You cannot just change the property directly and than call Update on a provider.

IsLockedOut — The value for this property is meant to reflect the side effect of previous login attempts or attempts to change a password using a question and answer challenge. As a result, it isn’t intended to be directly updatable through any APIs. Note that you can unlock a user with the UnlockUser method on MembershipUser.

LastLockoutDate — As with IsLockedOut, the value of this property is a side effect of an account being locked, or being explicitly unlocked. So, it is never intended to be directly updatable though the APIs.

CreationDate — This date/time is determined as a side effect of calling CreateUser. After a user is created, it doesn’t really make sense to go back and change this date.

LastPasswordChangedDate — As with other read-only properties, the value is changed as a side effect of calling either ChangePassword or ResetPassword. From a security perspective, it wouldn’t be a good idea to let arbitrary code change this type of data because then you wouldn’t have any guarantee of when a user actually triggered a password change.

IsOnline — This is actually a computed property as described earlier, so there is no need for a setter. You can indirectly influence this property by setting LastActivityDate.

ProviderName — When a MembershipUser instance is created, it must be associated with a valid provider. After this associated is established though, the Membership feature expects the same provider to manage the user instance for the duration of its lifetime. If this property were settable, you could end up with some strange results if you changed the value in between calls to the other public methods on the MembershipUser class.

379

Chapter 10

Among the properties that are public, Email, Comment, and IsApproved are pretty easy to understand. Email and Comment are just data fields, while IsApproved can be toggled between true and false — with a value of false causing ValidateUser to fail even if the correct username and password are supplied to the method.

LastActivityDate is public so that you can write other features that work with the Membership online tracking feature. For example, you could implement a custom feature that updates the user’s LastActivityDate each time user-specific data is retrieved. The ASP.NET SQL providers actually do this for Profile and Web Parts Personalization. However the ASP.NET SQL providers all use a common schema, so the Profile and Personalization providers perform the update from inside of the database. The LastActivityDate property allows for similar behavior but at the level of an object API as opposed to a data layer.

The last settable property on MembershipUser is the LastLoginDate property. However, leaving LastLoginDate as setable may seem a bit odd. It means that someone can write code to arbitrarily set when a user logged in — which of course means audit trails for logins can become suspect. Some developers though want to integrate the existing Membership providers with their own authentication systems. For these scenarios, there is the concept of multiple logins, and thus the desire to log a user account into an external system while having the Membership feature reflect when this external login occurred.

If you want to prevent LastLoginDate from being updatable (currently only the SQL provider even supports getting and setting this value), you can write a derived MembershipProvider that returns a derived MembershipUser instance. The derived MembershipUser instance can just throw a

NotSupportedException from the LastLoginDate setter.

DateTime Assumptions

There are quite a number of date related properties on the Membership feature, especially for the MembershipUser class. For smaller websites the question of how date-time values are handled is probably moot. In single-server environments, or web farms running in a single data center, server local time would be sufficient. However, as the feature was being iterated on a few things become pretty clear:

The ActiveDirectoryMembershipProvider relies on AD/ADAM for storage. The Active Directory store keeps track of significant time related data using UTC time — not server local time.

If in the future the feature is ever extended to officially support database replication with the SqlMembrshipProvider, then problems with running in multiple time zones will become an issue.

For both of these reasons, the code within the providers as well as within the core Membership classes was changed to instead use UTC time internally. Unlike the forms authentication feature that unfortunately has the quirk with using local times as opposed to UTC times, the desire was to have the Membership feature always work in UTC time to avoid problems with multiple time-zone support as well as clock adjustments (that is, daylight savings time).

Although the Membership feature doesn’t support database replication in ASP.NET 2.0 (it has never been tested), it is theoretically possible in future releases to have a network topology whereby different slices of Membership data are created in completely different time zones and then cross-replicated between different data centers. For this kind of scenario, having a common time measure is critical.

380

Membership

On a less theoretical note, it is likely that some websites will do things such as create new users right around the time server clocks are being adjusted. If information such as CreationDate were stored in machine local time, you would end up with some bizarre data records indicating that users were being created in the recent past or the soon-to-arrive future. Especially with security sensitive data this isn’t a desirable outcome.

Some folks may also have server deployments that span time zones. For example, you may have multiple data centers with web servers running into two different time zones — with each set of web servers pointed back to a central data center running your database servers. In this kind of scenario, which time zone do you pick? If you don’t use UTC time, you will always end up with weird date-time behavior because with this type of physical deployment some set of servers will always be in a different time zone than the time zone you selected for storing your data.

From a programming perspective, the .NET Framework traditionally returned machine local times from all public APIs. To handle this behavior while still handling UTC times internally, the Membership feature assumes that all date-time parameters passed in to public properties and methods to be in local time. Furthermore, whenever date-time data is returned from public properties and methods, data is always converted back to machine local time. Internally though, the core Membership classes as well as the default providers manipulate and store date-time data in UTC time. If you look at the data stored by the SqlMembershipProvider in a database, you will see that all the date-time-related columns appears to be wrong (assuming, of course, that you don’t actually live somewhere in the GMT time zone!). The reason is that by the time any Membership data is stored, the date-time-related variables have been converted to UTC time.

From the standpoint of someone using the Membership feature, this behavior should be mostly transparent to you. You can retrieve instances of MembershipUser objects, set date-related properties, or perform date related queries all using the local time your machine. The only potential for confusion occurs if you perform search queries using other features such as Profile that support date ranges for search parameters. If your query happens to span a time period when the clocks were reset, you will probably get slightly different results than if the Membership feature had stored data keyed off of a machine’s local time.

Within the Membership feature, the way in which UTC times are enforced is:

The various classes always call ToUniversalTime on any date-time parameters passed in to them.

The MembershipUser class calls ToUniversalTime on all date-time parameters for its constructor as well as in the setters for any public properties. This means that you can set a machine-local date time for a property like LastActivityDate, and MembershipUser will still ensure that it is treated as a UTC time internally. Due to the way the .NET Framework System.DateTime class works, you can actually pass UTC date-time parameters if you want to the MembershipUser class (or any class for that matter). This works because the result of calling

ToUniversalTime on a UTC System.DateTime is a no-op.

For public getters, the MembershipUser class calls ToLocalTime on date-time data prior to returning it. As a result, all data retrieved from the Membership feature will always reflect machine-local times.

The one thing you should do for your servers, both web servers and whatever back-end servers store Membership data, is to regularly synchronize your server clocks with a common time source. Although this recommendation isn’t made specifically because of any inherent problem with using UTC time, the implementation details for supporting UTC time highlight the need for synchronized clocks.

381