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  | 
Thursday, August 2, 2007 8:10:19 PM (Romance Daylight Time, UTC+02:00)
Could you provide an example of how to use this in a application that already has a user table that includes all of the fields required by the Provider model? I don't understand this phrase:

"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."

Thanks,
Stephen
Stephen
Friday, August 3, 2007 8:15:52 AM (Romance Daylight Time, UTC+02:00)
When I find some time I'll try to post an example.

About the sentence you mention, if you use a control like the CreateUserWizard control to create a user, it will call the Membership method CreateUser under the hood. The CreateUser method has the following parameters:

username
password
email
passwordQuestion
passwordAnswer
isApproved
providerUserKey

so if you store more data in your users table, the CreateUser method will leave it as null (if that is possible, it may rise an error if you have columns as not null). So, probably in this case the best is to insert the data manually in the table so you can set the proper values for all your data in the same place. The methods CheckPasswordPolicy, ValidatePassword, EncodePassword and GenerateSalt can be used before inserting to do the same kind of processing that the CreateUser method does when inserting.
Sunday, August 19, 2007 10:57:54 PM (Romance Daylight Time, UTC+02:00)
Thanks, Manuel. I'm looking forward to an example.

Stephen
Stephen
Sunday, August 26, 2007 12:12:32 AM (Romance Daylight Time, UTC+02:00)
Is it possible to set the HashAlgorithmType?

PS - I got things working. If I wanted to use the CheckPasswordPolicy, ValidatePassword, EncodePassword, and GenerateSalt, I'm guessing I would have to create a derived class to override these methods... and then use that class as my Provider. Does that sound right?
Sunday, August 26, 2007 12:45:41 AM (Romance Daylight Time, UTC+02:00)
Stephen,

I'm glad you got it working.

The hash algorithm that the provider use is the one specified by the hashAlgorithmType attribute on the Membership node (not in the provider).

Currently if you want to use CheckPasswordPolicy, ValidatePassword, etc you have to replicate most of the functionality of the CreateUser when you create a new user.

In my current unreleased build, I created a method with the following signature:

public virtual bool ValidateUserCreation(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status, out string generatedPassword, out string generatedPasswordSalt, out string generatedAnswer)

That does the same that the CreateUser method but without inserting it. It has more out arguments with the encoded fields for user creation.

The next version will have some changes to be more user friendly:
* the ValidateUserCreation method I mentioned above.
* a CreateUserWizardEx control (that inherits from the CreateUserWizard control) that lets you create your own user in the CreateUserEx event. The event give access to the out parameters exposed in the ValidateUserCreation method so the developer doesn't need to know much about the internal working of the provider to create users.
* added automatic user unlocking.
* some bugfixes

I can't give you an exact release date but it should be around october.
Friday, October 12, 2007 12:35:19 AM (Romance Daylight Time, UTC+02:00)
/// <summary>Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.</summary>
/// <returns>true if the membership provider requires a unique e-mail address; otherwise, false. The default is true.</returns>
public override bool RequiresUniqueEmail {
get { return _enablePasswordReset; }
}

typo?
whoever
Friday, October 12, 2007 12:45:23 AM (Romance Daylight Time, UTC+02:00)
// if there is a lockedOut column, there has to be a counter and datetime column to track failed passwords
if (!String.IsNullOrEmpty(_isLockedOutColumn) &&
((String.IsNullOrEmpty(_failedPasswordAttemptWindowStartColumn)
|| String.IsNullOrEmpty(_failedPasswordAttemptWindowStartColumn)) ||

(String.IsNullOrEmpty(_failedPasswordAnswerAttemptWindowStartColumn)
|| String.IsNullOrEmpty(_failedPasswordAnswerAttemptWindowStartColumn)))){

throw new ProviderException("Error, there is a isLockedOut column but no columns to track failed attempts are present.");
}

Why is each checked twice with ||?
whoever
Friday, October 12, 2007 12:49:09 AM (Romance Daylight Time, UTC+02:00)
_failedPasswordAttemptCountColumn?
whoever
Friday, October 12, 2007 11:11:46 AM (Romance Daylight Time, UTC+02:00)
whoever,

both are typos. The second one should be:

// if there is a lockedOut column, there has to be a counter and datetime column to track failed passwords
if (!String.IsNullOrEmpty(_isLockedOutColumn) &&
((String.IsNullOrEmpty(_failedPasswordAttemptWindowStartColumn) || String.IsNullOrEmpty(_failedPasswordAttemptCountColumn)) &&
(String.IsNullOrEmpty(_failedPasswordAnswerAttemptWindowStartColumn) || String.IsNullOrEmpty(_failedPasswordAnswerAttemptCountColumn)))){
throw new ProviderException("Error, there is a isLockedOut column but no columns to track failed attempts are present.");
}

Those were already fixed in the next version. I have been waiting to find some time to make a good sample before releasing version 2, but I think it will be better to release it now and the sample can always come later ;-)

Also the first version has a bug that didn't prevented locked out users from login IIRC. Keep an eye on the blog as I'll release version 2 today.
Friday, October 12, 2007 6:48:35 PM (Romance Daylight Time, UTC+02:00)
I remember seeing one more, also in those || || places, but couldn't remember where. You probably already caught them.

Thanks for the great code, it's so elegant and eye opening. I was trying to copy its functionalities in a different custom provider for our AD authentication with SQL everything else environment but couldn't match its flexibility. Now I'm hooked with NHibernate and trying to convert my whole application over ^_^

Couldn't wait for version 2. Too bad I'm a beginner spaghetti coder, my first reaction was to gut your code and took out everything I don't need. Sounds like a bad idea, huh?
whoever
Saturday, October 13, 2007 9:56:54 AM (Romance Daylight Time, UTC+02:00)
I don't remember how many typos I fixed, but of course there could be more.

I'm glad you liked the code and learned from it. The providers were designed with maxium flexibility in mind and I think I did a good job ;-) When I wrote the provider I really didn't need all the stuff I implemented (for example, I never use questions and answers for password retrieval) but it was just a bit more of effort in case sometime I need it and I like completeness :-D

A couple of hours before you wrote the comment version 1.2 was released: http://www.manuelabadia.com/blog/PermaLink,guid,27e22b2c-af95-4f4c-befd-1debc5841735.aspx
Thursday, February 7, 2008 10:26:40 AM (Romance Standard Time, UTC+01:00)
Hi,

thanks for your work. I searched for exactly a provider like that :)

But i have a little problem and can't solve it. When i want to start bei web project, the following error message appears in the browser. I use SQL2005, .net 2.0, nhibernate. 1.2 GA. It seems, that it cant "find" the role provider, but i have no idea why. i used your dll and xml file and also the posted config.


Configuration Error
Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

Parser Error Message: Could not compile the mapping document: (XmlDocument)

Source Error:


Line 77: <roleManager defaultProvider="NHCustomRoleProvider" enabled="true">
Line 78: <providers>
Line 79: <add name="NHCustomRoleProvider" type="NHCustomProviders.NHCustomRoleProvider, NHCustomProviders"
Line 80: userTableName="User"
Line 81: userIdColumn="User_ID"
Junsas
Thursday, May 29, 2008 1:27:29 PM (Romance Daylight Time, UTC+02:00)
Hi,
Excellent article. Thanks for your work.
When i tried to use this code, i am getting an error "Could not load file or assembly 'NHibernate ....".
What should i do?
Raja
Raja
Friday, July 25, 2008 10:58:21 AM (Romance Daylight Time, UTC+02:00)
There is a problem in the current released build that prevents the membership and role provider from working if you do not configure it using the nhibernate section handler instead of the hibernate-configuration.
Thursday, December 11, 2008 9:36:49 PM (Romance Standard Time, UTC+01:00)
I am getting the following error when trying to run the sample:

The type initializer for 'NHibernate.Proxy.Poco.Castle.CastleProxyFactory' threw an exception

Any ideas?
Biuta
Sunday, December 14, 2008 10:49:53 AM (Romance Standard Time, UTC+01:00)
Biuta,

I've never seen that error. Are you getting it in v2.0?
Wednesday, May 27, 2009 4:14:09 PM (Romance Daylight Time, UTC+02:00)
"There is a problem in the current released build that prevents the membership and role provider from working if you do not configure it using the nhibernate section handler instead of the hibernate-configuration."

Have you fixed this in later releases? Or is it still an issue?
Mark Holtman
Wednesday, May 27, 2009 4:19:01 PM (Romance Daylight Time, UTC+02:00)
Mark,

it is fixed in the latest version.
Tuesday, August 11, 2009 3:23:49 PM (Romance Daylight Time, UTC+02:00)
Hi,

Does this work with ASP.Net AJAX too ? I've tried your demo and I've added support for ASP.net AJAX. Then I added a button on the masterpage and on the onclick event I call:

Sys.Services.AuthenticationService.get_isLoggedIn()

This always returns false whether the user is logged in or not.... So should I assume it doesn't work ?

Thanks !
Sam
Tuesday, August 11, 2009 3:31:17 PM (Romance Daylight Time, UTC+02:00)
I'm sorry, it was my mistake... I forgot the following in my web.config file :

<system.web.extensions>
<scripting>
<webServices>
<authenticationService enabled="true" />
</webServices>
</scripting>
</system.web.extensions>

Your provider is great !
Sam
Tuesday, August 11, 2009 4:05:20 PM (Romance Daylight Time, UTC+02:00)
Actually I do have another question.

From what I understand, the mapping file for the USERS table is dynamically generated based on the parameters of the membership provider.

What if I want to add a bag to my USERS mapping file ? Or any other fields... I'd like to keep my own USERS table and the mapping file I've already created, is that possible ?

Thanks
Sam
Tuesday, August 11, 2009 5:48:20 PM (Romance Daylight Time, UTC+02:00)
I was trying to update a User for setting its isApproved column to true but I get an exception.

Here's my code :

NHCustomMembershipProvider membershipProvider = (NHCustomMembershipProvider)Membership.Providers["NHCustomProviderFull"];
MembershipUser user = membershipProvider.GetUser("sam3",false);
user.IsApproved = true;
membershipProvider.UpdateUser(user);

Exception is raised in CheckUtcDateTimeKind :

Error, DateTime is not UTC for property LastPasswordChangedDate [id = 6]

I guess this is because the value of the date is 01/01/0001. How can we fix that ?

Thanks
Sam
Tuesday, August 11, 2009 6:25:33 PM (Romance Daylight Time, UTC+02:00)
Hi again,

Here's the problem. The LastLoginDate, LastPasswordChangedDate and LastLockOutDate are all nullable fields in the database.

However in the function CreateMembershipUser your code looks like this :

return new MembershipUser(providerName, UserName, UserId, Email,
PasswordQuestion, Comments, IsApproved, IsLockedOut, CreationDate,
LastLoginDate ?? DateTime.MinValue, LastActivityDate,
LastPasswordChangedDate ?? DateTime.MinValue, LastLockOutDate ?? DateTime.MinValue);

What this means is that because they are null by default, there are going to be set to 01/01/0001 by doing DateTime.MinValue.

Now when you try to update the user, by setting isApproved to true, NHibernate detects that the dates have changed too (they are no longer null), but because they are not in a valid format (SQL Server does not recognize that kind of non-sense date...), an exception is raised (even if I by-pass your UTC bit of code).

The problem seems that the constructor of MembershipUser does not accept null dates, which is rubbish... So the only work-around I can think of right now, is to set a default value for these dates in the database when a record is created.

If you have a better idea, let me know please.

Thanks
Sam
Wednesday, August 12, 2009 3:03:05 PM (Romance Daylight Time, UTC+02:00)
I've tried to change the type of the userid from int to uniqueidentifier.
In the mapping it looks like :
userIdColumn="userId" userIdType="guid" idGeneratorClass="guid.comb"

Now when I create a new user, I get the following exception :

Cannot insert the value NULL into column 'userId', table 'NHCustomProvidersTest.dbo.Users'; column does not allow nulls. INSERT fails.
The statement has been terminated.

Could it be that I can't use the guid type ? Normally it works fine with my other tables and NHIbernate. Do you know what's wrong ?
sam
Wednesday, August 12, 2009 6:01:16 PM (Romance Daylight Time, UTC+02:00)
Actually I figured out the above, don't worry about it.

However I've run into a big issue for my application. I'm creating my SessionFactory in the ApplicationStart event. How difficult would it be to change your provider so that it can use an existing SessionFactory, instead of creating and configuring a new one ? Because of that, it doesn't seem to work for me...
sam
Monday, August 17, 2009 12:45:18 PM (Romance Daylight Time, UTC+02:00)
From your sample, running the ASP.net configuration tool and clicking on the Security tab lead to the following exception :

Type is not resolved for member 'NHibernate.ADOException,NHibernate, Version=2.0.1.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4'.

Is it possible to use the tool with your Membership provider ?
Thanks
Sam
Thursday, June 9, 2011 9:53:47 AM (Romance Daylight Time, UTC+02:00)
To the other data field in Users table.

The Users table column name changing error?
Can not Company column name change?

Error Message:
{“could not insert: [NHCustomProviderTest.Model.User][SQL: INSERT INTO users (Name, Password, EmailAddress, LastLogon, LogonId, --, ---, ---, ---) VALUES (?, ?, ?, ?, ?,?,?,?)] ; select IDENTY SCOPE()“} .


How do I change a column name, or add coulmn?


ohts
All comments require the approval of the site owner before being displayed.
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

[Captcha]Enter the code shown (prevents robots):

Live Comment Preview
Copyright © 2019 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.