Custom Membership, Role Providers, Membership User Series.
Custom Membership Provider
There are many times when the MembershipProvider and its
underlying database construction aren’t sufficient enough for our needs. As
MSDN states there are two reasons why one would want a custom
MembersipProvider:
- You
need to store membership information in a data source that is not
supported by the membership providers included with the .NET Framework,
such as a FoxPro database, an Oracle database, or other data sources.
- You need to manage membership information using a database schema that is different from the database schema used by the providers that ship with the .NET Framework. A common example of this would be membership data that already exists in a SQL Server database for a company or Web site.
To implement a custom membership provider, you create a
class that inherits the MembershipProvider abstract class from
the System.Web.Security namespace.
The MembershipProvider
abstract class inherits the ProviderBase abstract class from
the System.Configuration.Provider namespace,
so you must implement the required members of the ProviderBase class
as well.
For example this custom membership provider uses LINQ-to-SQL and my own tables in MS SQL Server to store and retrieve membership information in my database:
namespace Custom.Membership
{
using System;
using System.Linq;
using System.Configuration;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Data;
using System.Data.SqlClient;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
using System.Web.Security;
using MCA.Custom.CustomData;
public sealed class CustomMembershipProvider : MembershipProvider
{
#region Class Variables
private int newPasswordLength = 8;
private string connectionString;
private string applicationName;
private bool enablePasswordReset;
private bool enablePasswordRetrieval;
private bool requiresQuestionAndAnswer;
private bool requiresUniqueEmail;
private int maxInvalidPasswordAttempts;
private int passwordAttemptWindow;
private MembershipPasswordFormat passwordFormat;
private int minRequiredNonAlphanumericCharacters;
private int minRequiredPasswordLength;
private string passwordStrengthRegularExpression;
private MachineKeySection machineKey; //Used when determining encryption key values.
#endregion
#region Properties
public override string ApplicationName
{
get
{
return applicationName;
}
set
{
applicationName = value;
}
}
public override bool EnablePasswordReset
{
get
{
return enablePasswordReset;
}
}
public override bool EnablePasswordRetrieval
{
get
{
return enablePasswordRetrieval;
}
}
public override bool RequiresQuestionAndAnswer
{
get
{
return requiresQuestionAndAnswer;
}
}
public override bool RequiresUniqueEmail
{
get
{
return requiresUniqueEmail;
}
}
public override int MaxInvalidPasswordAttempts
{
get
{
return maxInvalidPasswordAttempts;
}
}
public override int PasswordAttemptWindow
{
get
{
return passwordAttemptWindow;
}
}
public override MembershipPasswordFormat PasswordFormat
{
get
{
return passwordFormat;
}
}
public override int MinRequiredNonAlphanumericCharacters
{
get
{
return minRequiredNonAlphanumericCharacters;
}
}
public override int MinRequiredPasswordLength
{
get
{
return minRequiredPasswordLength;
}
}
public override string PasswordStrengthRegularExpression
{
get
{
return passwordStrengthRegularExpression;
}
}
#endregion
#region MembershipProvider overrides
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
{
string configPath = "~/web.config";
Configuration NexConfig = WebConfigurationManager.OpenWebConfiguration(configPath);
MembershipSection section = (MembershipSection)NexConfig.GetSection("system.web/membership");
ProviderSettingsCollection settings = section.Providers;
NameValueCollection membershipParams = settings[section.DefaultProvider].Parameters;
config = membershipParams;
}
if (name == null || name.Length == 0)
{
name = "CustomMembershipProvider";
}
if (String.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "Custom Membership Provider");
}
//Initialize the abstract base class.
base.Initialize(name, config);
applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredAlphaNumericCharacters"], "1"));
minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], String.Empty));
enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));
string temp_format = config["passwordFormat"];
if (temp_format == null)
{
temp_format = "Hashed";
}
switch (temp_format)
{
case "Hashed":
passwordFormat = MembershipPasswordFormat.Hashed;
break;
case "Encrypted":
passwordFormat = MembershipPasswordFormat.Encrypted;
break;
case "Clear":
passwordFormat = MembershipPasswordFormat.Clear;
break;
default:
throw new ProviderException("Password format not supported.");
}
ConnectionStringSettings ConnectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]];
if ((ConnectionStringSettings == null) || (ConnectionStringSettings.ConnectionString.Trim() == String.Empty))
{
throw new ProviderException("Connection string cannot be blank.");
}
connectionString = ConnectionStringSettings.ConnectionString;
//Get encryption and decryption key information from the configuration.
System.Configuration.Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
machineKey = cfg.GetSection("system.web/machineKey") as MachineKeySection;
if (machineKey.ValidationKey.Contains("AutoGenerate"))
{
if (PasswordFormat != MembershipPasswordFormat.Clear)
{
throw new ProviderException("Hashed or Encrypted passwords are not supported with auto-generated keys.");
}
}
}
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(oldPassword) || string.IsNullOrWhiteSpace(newPassword)) return false;
if (oldPassword == newPassword) return false;
CustomMembershipUser user = GetUser(username);
if (user == null) return false;
CustomDataDataContext db = new CustomDataDataContext();
var RawUser = (from u in db.Users
where u.UserName == user.UserName && u.DeletedOn == null
select u).FirstOrDefault();
if (string.IsNullOrWhiteSpace(RawUser.Password)) return false;
RawUser.Password = EncodePassword(newPassword);
db.SubmitChanges();
return true;
}
public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
{
throw new NotImplementedException();
}
/// <summary>
/// Create custom CustomMembershipUser.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="email"></param>
/// <param name="passwordQuestion"></param>
/// <param name="passwordAnswer"></param>
/// <param name="isApproved"></param>
/// <param name="providerUserKey"></param>
/// <param name="status"></param>
/// <param name="companyID"></param>
/// <param name="name"></param>
/// <returns></returns>
public CustomMembershipUser CreateUser(
string username,
string password,
string email,
string passwordQuestion,
string passwordAnswer,
bool isApproved,
object providerUserKey,
out MembershipCreateStatus status,
int companyID,
string name,
string phoneNumber)
{
ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);
OnValidatingPassword(args);
if (args.Cancel)
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
if ((RequiresUniqueEmail && (GetUserNameByEmail(email) != String.Empty)))
{
status = MembershipCreateStatus.DuplicateEmail;
return null;
}
CustomMembershipUser CustomMembershipUser = GetUser(username);
if (CustomMembershipUser == null)
{
try
{
using (CustomDataDataContext _db = new CustomDataDataContext())
{
User user = new User();
user.CompanyFK = companyID;
user.Name = name;
user.UserName = username;
user.Password = EncodePassword(password);
user.Email = email.ToLower();
user.CreatedOn = DateTime.Now;
user.ModifiedOn = DateTime.Now;
user.Phone = phoneNumber;
_db.Users.InsertOnSubmit(user);
_db.SubmitChanges();
status = MembershipCreateStatus.Success;
return GetUser(username);
}
}
catch
{
status = MembershipCreateStatus.ProviderError;
}
}
else
{
status = MembershipCreateStatus.DuplicateUserName;
}
return null;
}
/// <summary>
/// Createa MembershipUser.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="email"></param>
/// <param name="passwordQuestion"></param>
/// <param name="passwordAnswer"></param>
/// <param name="isApproved"></param>
/// <param name="providerUserKey"></param>
/// <param name="status"></param>
/// <returns></returns>
public override MembershipUser CreateUser(
string username,
string password,
string email,
string passwordQuestion,
string passwordAnswer,
bool isApproved,
object providerUserKey,
out MembershipCreateStatus status)
{
ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);
OnValidatingPassword(args);
if (args.Cancel)
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
if ((RequiresUniqueEmail && (GetUserNameByEmail(email) != String.Empty)))
{
status = MembershipCreateStatus.DuplicateEmail;
return null;
}
MembershipUser membershipUser = GetUser(username, false);
if (membershipUser == null)
{
try
{
using (CustomDataDataContext _db = new CustomDataDataContext())
{
User user = new User();
user.CompanyFK = 0;
user.Name = "";
user.UserName = username;
user.Password = EncodePassword(password);
user.Email = email.ToLower();
user.CreatedOn = DateTime.Now;
user.ModifiedOn = DateTime.Now;
_db.Users.InsertOnSubmit(user);
_db.SubmitChanges();
status = MembershipCreateStatus.Success;
return GetUser(username, false);
}
}
catch
{
status = MembershipCreateStatus.ProviderError;
}
}
else
{
status = MembershipCreateStatus.DuplicateUserName;
}
return null;
}
/// <summary>
/// Delete user
/// </summary>
/// <param name="username"></param>
/// <param name="deleteAllRelatedData"></param>
/// <returns></returns>
public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
bool ret = false;
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
User user = (from u in _db.Users
where u.UserName == username && u.DeletedOn == null
select u).FirstOrDefault();
if (user != null)
{
_db.Users.DeleteOnSubmit(user);
_db.SubmitChanges();
ret = true;
}
}
catch
{
ret = false;
}
}
return ret;
}
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override int GetNumberOfUsersOnline()
{
throw new NotImplementedException();
}
public override string GetPassword(string username, string answer)
{
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
var pass = (from p in _db.Users
where p.UserName == username && p.DeletedOn == null
select p.Password).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(pass))
return UnEncodePassword(pass);
}
catch { }
}
return null;
}
public CustomMembershipUser GetUser(string username)
{
CustomMembershipUser CustomMembershipUser = null;
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
User user = (from u in _db.Users
where u.UserName == username && u.DeletedOn == null
select u)
.FirstOrDefault();
if (user != null)
{
CustomMembershipUser = new CustomMembershipUser(
this.Name,
user.UserName,
null,
user.Email,
"",
"",
true,
false,
user.CreatedOn,
DateTime.Now,
DateTime.Now,
default(DateTime),
default(DateTime),
user.CompanyFK,
user.Name);
}
}
catch
{
}
}
return CustomMembershipUser;
}
/// <summary>
/// Get MembershipUser.
/// </summary>
/// <param name="username"></param>
/// <param name="userIsOnline"></param>
/// <returns></returns>
public override MembershipUser GetUser(string username, bool userIsOnline)
{
MembershipUser membershipUser = null;
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
User user = (from u in _db.Users
where u.UserName == username && u.DeletedOn == null
select u)
.FirstOrDefault();
if (user != null)
{
membershipUser = new MembershipUser(this.Name,
user.UserName,
null,
user.Email,
"",
"",
true,
false,
user.CreatedOn,
DateTime.Now,
DateTime.Now,
default(DateTime),
default(DateTime));
}
}
catch
{
}
}
return membershipUser;
}
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
throw new NotImplementedException();
}
public override string GetUserNameByEmail(string email)
{
throw new NotImplementedException();
}
public override string ResetPassword(string username, string answer)
{
throw new NotImplementedException();
}
public override bool UnlockUser(string userName)
{
throw new NotImplementedException();
}
public override void UpdateUser(MembershipUser user)
{
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
User userToEdit = (from u in _db.Users
where u.UserName == user.UserName && u.DeletedOn == null
select u).FirstOrDefault();
if (userToEdit != null)
{
// submit changes
//_db.SubmitChanges();
}
}
catch
{
}
}
}
public void UpdateCustomUser(CustomMembershipUser user)
{
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
User userToEdit = (from u in _db.Users
where u.UserName == user.UserName && u.DeletedOn == null
select u).FirstOrDefault();
if (userToEdit != null)
{
userToEdit.Name = user.Name;
userToEdit.Email = user.Email;
userToEdit.CompanyFK = user.CompanyFK;
// submit changes
_db.SubmitChanges();
}
}
catch
{
}
}
}
/// <summary>
/// Validate user.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
public override bool ValidateUser(string username, string password)
{
bool isValid = false;
using (CustomDataDataContext _db = new CustomDataDataContext())
{
try
{
User user = (from u in _db.Users
where u.UserName == username && u.DeletedOn == null
select u).FirstOrDefault();
if (user != null)
{
string storedPassword = user.Password;
if (CheckPassword(password, storedPassword))
{
isValid = true;
}
}
}
catch
{
isValid = false;
}
}
return isValid;
}
#endregion
#region Utility Methods
/// <summary>
/// Check the password format based upon the MembershipPasswordFormat.
/// </summary>
/// <param name="password">Password</param>
/// <param name="dbpassword"></param>
/// <returns></returns>
/// <remarks></remarks>
private bool CheckPassword(string password, string dbpassword)
{
string pass1 = password;
string pass2 = dbpassword;
switch (PasswordFormat)
{
case MembershipPasswordFormat.Encrypted:
pass2 = UnEncodePassword(dbpassword);
break;
case MembershipPasswordFormat.Hashed:
pass1 = EncodePassword(password);
break;
default:
break;
}
if (pass1 == pass2)
{
return true;
}
return false;
}
/// <summary>
/// UnEncode password.
/// </summary>
/// <param name="encodedPassword">Password.</param>
/// <returns>Unencoded password.</returns>
private string UnEncodePassword(string encodedPassword)
{
string password = encodedPassword;
switch (PasswordFormat)
{
case MembershipPasswordFormat.Clear:
break;
case MembershipPasswordFormat.Encrypted:
password =
Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
break;
case MembershipPasswordFormat.Hashed:
//HMACSHA1 hash = new HMACSHA1();
//hash.Key = HexToByte(machineKey.ValidationKey);
//password = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
throw new ProviderException("Not implemented password format (HMACSHA1).");
default:
throw new ProviderException("Unsupported password format.");
}
return password;
}
/// <summary>
/// Get config value.
/// </summary>
/// <param name="configValue"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
private string GetConfigValue(string configValue, string defaultValue)
{
if (String.IsNullOrEmpty(configValue))
{
return defaultValue;
}
return configValue;
}
/// <summary>
/// Encode password.
/// </summary>
/// <param name="password">Password.</param>
/// <returns>Encoded password.</returns>
private string EncodePassword(string password)
{
string encodedPassword = password;
switch (PasswordFormat)
{
case MembershipPasswordFormat.Clear:
break;
case MembershipPasswordFormat.Encrypted:
byte[] encryptedPass = EncryptPassword(Encoding.Unicode.GetBytes(password));
encodedPassword = Convert.ToBase64String(encryptedPass);
break;
case MembershipPasswordFormat.Hashed:
HMACSHA1 hash = new HMACSHA1();
hash.Key = HexToByte(machineKey.ValidationKey);
encodedPassword =
Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
break;
default:
throw new ProviderException("Unsupported password format.");
}
return encodedPassword;
}
/// <summary>
/// Converts a hexadecimal string to a byte array. Used to convert encryption key values from the configuration
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
/// <remarks></remarks>
private byte[] HexToByte(string hexString)
{
byte[] returnBytes = new byte[hexString.Length / 2];
for (int i = 0; i < returnBytes.Length; i++)
returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
return returnBytes;
}
#endregion
}
Of course, not all the class members are implemented, but
for illustration I think the example is long enough so that you can get the
point.
There are some methods
here like:
public CustomMembershipUser GetUser(string username)
I deliberately put that there so that I can illustrate that you could return a custom membership user. We will discuss this in one of the next parts of these series.
In order for this to work you need to tell the web
application that we are going to use a custom membership provider. So, add the
following line to web.config:
<membership defaultProvider="CustomMembershipProvider">
<providers>
<clear />
<add name="CustomMembershipProvider" type="Custom.Membership.CustomMembershipProvider" connectionStringName="CustomConnectionString" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" passwordFormat="Encrypted" />
</providers>
</membership>
This way you are telling the application which provider to
use and initialize its members with default values.
Now, to the other most important part of this - the usage.
The default usage is pretty straightforward i.e. you just call any of the
methods trough the Membership custom class:
if(Membership.ValidateUser(userName.Text,
password.Text))
{
// do something
}
You might have added extra methods to your custom membership
class, like IsUserActive(string
username) in that case you can get your custom provider trough the
Provider or Providers properties of Membership and call the method:
CustomMembershipProvider customMemebership = (CustomMembershipProvider)System.Web.Security.Membership.Providers["CustomMembershipProvider"];
bool active = customMembership.IsUserActive(username);
Hi there, I'm fairly new to .net. I'm currently implementing .net framework 4.5 mvc3 using mysql as a database. Would this article be applicable to my case. T
ReplyDeleteRegards,
Best
Hi Best,
ReplyDeleteYou could implement the entire provider just using interface. For these interfaces you could later make implementations in whichever db you want and inject the dependency.
In my post I'm just doing it using Linq-To-Sql as an illustration. I would definetly do it using interfaces if it was for production. That way you would not be depending on any concrete implementation or usage of any concrete db, but sort of "plug in" any of them you want.
Cheers,
Bojan
I can follow your process to create custom provider with mysql database for sharepoint FBA
ReplyDeletecan you provide me with the database table fields schema. Im using mysql
ReplyDelete