Blog Home  Home Feed your aggregator (RSS 2.0)  
Custom Membership and Role Providers using NHibernate - Manuel Abadia's ASP.NET stuff
 
# Sunday, April 8, 2007

Usually, the built-in providers for ASP.NET do not integrate very well with existing applications. The membership provider has a very complex structure that in most cases is not needed at all. The following diagram shows most of the tables and relations for the built-in ASP.NET providers:

 

The membership related tables are selected in red and the role related tables are selected in blue. The default membership and roles provider have support to store data for multiple applications using the aspnet_Applications table. I think this table was introduced because some portal frameworks like dotnetnuke needed the ability to store membership and role data for multiple users in a single store because they support multiple portals. If you don't need this scenario (and most applications do not need it), this only complicate things.

Also, the built-in providers use tables that are not part of your application tables, and some of those tables have quite a few columns, and probably some are not needed by your application.

If you do not use SQL Server, you can not use the default providers, so that is a big showstopper.

To overcome all of this problems I have created a custom membership and role provider that use NHibernate and can be easily integrated with existing or future applications using your own tables.

The reference book about providers is:

Professional ASP.NET 2.0 Security, Membership, and Role Management.

I suggest you to read it if you really want to get deep into providers as it is an excellent book.

The NHCustomMembershipProvider uses NHibernate to do all membership related database operations so it should work on all databases supported by NHibernate. The membership data will be stored and retrieved from a table configured in the provider section in the web.config so you can map it to an existing table like users. The column names for your user table can also be configured in the provider section. The cool thing about the provider is that it adapts its behaviour to the number of columns supported by your data store. For example, if you use the provider and map it to your users table where you only have the membership data for userId, userName, password and email, it will work without any problem. If your data store supports more advanced options of the membership provider like account lockout, password question and answer, password salt or last activity date, that advanced functionality will be automatically available, but it is not required.

The provider knows what functionality to offer based on the number of columns that are mapped to your data store. To support account lockout when the user do not supply a valid password after some tries in a predefined period of time you need to map the isLockedOutColumn, failedPasswordAttemptCountColumn and failedPasswordAttemptWindowStartColumn to columns in your data store.

The default configuration for the provider is to use integer identities or sequences for the user identifier, but that can be changed using the userIdType and idGeneratorClass properties in the provider configuration section.

If you use the provider with an existing user table, probably you will create the users manually without using the CreateUser method from the membership provider. The provider exposes the methods CheckPasswordPolicy, ValidatePassword, EncodePassword and GenerateSalt to allow manual creation of the user in case you need it, without sacrifying password strength.

To finish with the custom membership provider lets see an example. If you want to use the membership provider with a table like this (that has columns for all supported functionality):

CREATE TABLE [dbo].[Users](

 [userId] [int] IDENTITY(1,1) NOT NULL,

 [username] [nvarchar](50) NOT NULL,

 [email] [nvarchar](100) NOT NULL,

 [password] [nvarchar](256) NOT NULL,

 [passwordSalt] [nvarchar](64) NULL,

 [passwordFormat] [int] NOT NULL,

 [passwordQuestion] [nvarchar](512) NOT NULL,

 [passwordAnswer] [nvarchar](512) NOT NULL,

 [failedPasswordAttemptCount] [int] NOT NULL,

 [failedPasswordAttemptWindowStart] [smalldatetime] NULL,

 [failedPasswordAnswerAttemptCount] [int] NOT NULL,

 [failedPasswordAnswerAttemptWindowStart] [smalldatetime] NULL,

 [lastPasswordChangedDate] [smalldatetime] NULL,

 [creationDate] [smalldatetime] NOT NULL,

 [lastActivityDate] [smalldatetime] NOT NULL,

 [isApproved] [bit] NOT NULL,

 [isLockedOut] [bit] NOT NULL,

 [lastLockOutDate] [smalldatetime] NULL,

 [lastLoginDate] [smalldatetime] NULL,

 [name] [nvarchar](256) NULL,

 [surname] [nvarchar](256) NULL,

 [address] [nvarchar](256) NULL,

 [phone] [nvarchar](16) NULL,

 [postalCode] [nvarchar](50) NULL,

 [comments] [nvarchar](max) NULL,

    CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED

    (

    [userId] ASC

    )

)

The configuration section for the membership provider will be something like this:

<membership defaultProvider="NHCustomMembershipProvider">

  <providers>

    <add name="NHCustomMembershipProvider" type="NHCustomProviders.NHCustomMembershipProvider, NHCustomProviders"

        enablePasswordReset="true" enablePasswordRetrieval="true"

        minRequiredNonAlphanumericCharacters="1" minRequiredPasswordLength="6" passwordFormat="Encrypted"

        maxInvalidPasswordAttempts="5" passwordAttemptWindow="5"

        requiresQuestionAndAnswer="true"

        requiresUniqueEmail="false"

 

        tableName="USERS"

        userIdColumn="USERID"

        userIdType="Int32"

        idGeneratorClass="native"

        userNameColumn="USERNAME"

        emailColumn="EMAIL"

        passwordColumn="PASSWORD"

        passwordSaltColumn="PASSWORDSALT"

        passwordFormatColumn="PASSWORDFORMAT"

        passwordQuestionColumn="PASSWORDQUESTION"

        passwordAnswerColumn="PASSWORDANSWER"

        failedPasswordAttemptCountColumn="FAILEDPASSWORDATTEMPTCOUNT"

        failedPasswordAttemptWindowStartColumn="FAILEDPASSWORDATTEMPTWINDOWSTART"

        failedPasswordAnswerAttemptCountColumn="FAILEDPASSWORDANSWERATTEMPTCOUNT"

        failedPasswordAnswerAttemptWindowStartColumn="FAILEDPASSWORDANSWERATTEMPTWINDOWSTART"

        lastPasswordChangedDateColumn="LASTPASSWORDCHANGEDDATE"

        creationDateColumn="CREATIONDATE"

        lastActivityDateColumn="LASTACTIVITYDATE"

        isApprovedColumn="ISAPPROVED"

        isLockedOutColumn="ISLOCKEDOUT"

        lastLockOutDateColumn="LASTLOCKOUTDATE"

        lastLoginDateColumn="LASTLOGINDATE"

        commentsColumn="COMMENTS"

 

        userNameColumnSize="50"

        emailColumnSize="50"

        passwordColumnSize="256"

        passwordSaltColumnSize="64"

        passwordQuestionColumnSize="512"

        passwordAnswerColumnSize="512"

        passwordSaltSize="16"

 

        raiseSystemEvents="true"

        dynamicUpdate="true"

    />

  </providers>

</membership>


   
The column sizes are optional but it is a good practice to fill them.

The built-in provider raises events after a succesfully or a failed login. However, ASP.NET 2.0 does not allow to create or raise built-in health monitoring events, so I had to use reflection in a private method in order to raise the same events. That means that the provider will not run in medium trust if it is not installed in the GAC if you want to raise the system events. As this can be a showstopper to some people I have added an option to disable system events generation (raiseSystemEvents).

The provider reads the config section and programatically generates a mapping for the specified columns in the ConfigureNHibernate method. The provider expects that you have configured NHibernate in the web.config to work. If you need extra configuration for the provider you can inherit from the provider and use override the ConfigureNHibernate method, modifying the Configuration object that the base class returns.

The NHCustomRoleProvider is also configurable in order adapt to your own tables. It only needs a Users table with id and user name columns, a roles table with id and role name columns, and a join table used to store the information for the many-to-many relationship between the users and roles.

For example, if we the previous user table with the following tables (FKs omitted for brevity):

CREATE TABLE [dbo].[Roles](

    [roleId] [int] IDENTITY(1,1) NOT NULL,

    [name] [nvarchar](50) NOT NULL,

    CONSTRAINT [PK_Roles] PRIMARY KEY CLUSTERED

    (

       [roleId] ASC

    )

)

 

CREATE TABLE [dbo].[Users_In_Roles](

    [userId] [int] NOT NULL,

    [roleId] [int] NOT NULL,

    CONSTRAINT [PK_UsersInRoles] PRIMARY KEY CLUSTERED

    (

       [userId] ASC,

       [roleId] ASC

    )

)

 

The configuration section for the role provider will be something like this:

<roleManager defaultProvider="NHCustomRoleProvider" enabled="true">

  <providers>

    <add name="NHCustomRoleProvider" type="NHCustomProviders.NHCustomRoleProvider, NHCustomProviders"

        userTableName="USERS"

        userIdColumn="USERID"

        userIdType="Int32"

        userIdGeneratorClass="native"

        userNameColumn="USERNAME"

 

        roleTableName="ROLES"

        roleIdColumn="ROLEID"

        roleIdType="Int32"

        roleIdGeneratorClass="native"

        roleNameColumn="NAME"

 

        joinTableName="USERS_IN_ROLES"

 

        userNameColumnSize="50"

        roleNameColumnSize="50"

    />

  </providers>

</roleManager>


Note that the id columns in the join table need be named as in their respective tables in order to work.
   
Programatically creating the mapping using the CreateMappings for something more complex than a single table seems to be something very tied to the NHibernate implementation so I opted to create a XmlDocument with the mapping read from the configuration settings as it seems to be the best thing to do in complex cases.

One of the problems I faced with the NHCustomRoleProvider is that some times I wanted to save or delete data from one side of the many-to-many relationship but in some cases I wanted to use the other side. If you know a bit of NHibernate you know that the inverse attribute controls what side of the relationship has to be used in order to have changes persisted. As I was generating the mapping dynamically I created two session factories, one with the inverse set to true on one side and the other with the inverse in the other side. That way I open a connection in the session factory that has the mapping with the inverse in the side of the relationship I want.

I'd appreciate usage feedback, suggestions, bug reports, etc. I have only tried these providers with NHibernate 1.2.0 GA and the MsSql2005Dialect so let me know if it works in other configurations.

Please do not store the providers in your server. Link to this page instead. Updates to the providers will be posted in this page.

History

v1.0 - 08/04/2007

v1.1 - 21/06/2007

  • Now using NHibernate 1.2.0 GA
  • Some minor fixes

v1.2 - 12/10/2007:

  • Fixed an important bug that didn’t prevent locked out users from login in.
  • Added a CreateUserWizardEx control that allows creating a user with extra information in an easy way.
  • Added auto unlock support.
  • Fixed some minor bugs/typos.

For more details about the features introduced in version 1.2 take a look here.

v2.0 - 01/08/2008:

For more details about the features introduced in version 2.0 and a sample of its usage take a look here.

NHCustomProviders_bin.zip (67 KB)
NHCustomProviders_source.zip (122 KB)

Sunday, April 8, 2007 6:21:02 PM (Romance Daylight Time, UTC+02:00)  #    Comments [27]   ASP.NET | Microsoft .NET Framework | NHibernate  | 
Copyright © 2014 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.