2017-07-20 11:21:28 +02:00
using System ;
2013-11-07 17:16:22 +01:00
using System.Collections.Specialized ;
using System.Configuration.Provider ;
using System.Security.Cryptography ;
using System.Text ;
using System.Text.RegularExpressions ;
2015-05-11 12:22:56 +10:00
using System.Web ;
2013-11-07 17:16:22 +01:00
using System.Web.Configuration ;
using System.Web.Hosting ;
using System.Web.Security ;
2017-05-30 15:46:25 +02:00
using Umbraco.Core.Composing ;
2014-01-02 15:28:42 +11:00
using Umbraco.Core.Logging ;
2017-05-12 14:49:44 +02:00
using Umbraco.Core.Models ;
2013-11-07 17:16:22 +01:00
namespace Umbraco.Core.Security
{
/// <summary>
/// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing.
/// </summary>
public abstract class MembershipProviderBase : MembershipProvider
{
2015-02-09 17:37:21 +11:00
public string HashPasswordForStorage ( string password )
{
string salt ;
var hashed = EncryptOrHashNewPassword ( password , out salt ) ;
return FormatPasswordForStorage ( hashed , salt ) ;
}
public bool VerifyPassword ( string password , string hashedPassword )
{
2017-08-25 17:55:26 +02:00
if ( string . IsNullOrWhiteSpace ( hashedPassword ) ) throw new ArgumentException ( "Value cannot be null or whitespace." , "hashedPassword" ) ;
2015-02-09 17:37:21 +11:00
return CheckPassword ( password , hashedPassword ) ;
}
2013-11-07 17:16:22 +01:00
/// <summary>
2017-08-25 17:55:26 +02:00
/// Providers can override this setting, default is 10
2013-11-07 17:16:22 +01:00
/// </summary>
2014-01-02 15:28:42 +11:00
public virtual int DefaultMinPasswordLength
2013-11-07 17:16:22 +01:00
{
2017-08-25 17:55:26 +02:00
get { return 10 ; }
2013-11-07 17:16:22 +01:00
}
/// <summary>
2017-08-25 17:55:26 +02:00
/// Providers can override this setting, default is 0
2013-11-07 17:16:22 +01:00
/// </summary>
2014-01-02 15:28:42 +11:00
public virtual int DefaultMinNonAlphanumericChars
2013-11-07 17:16:22 +01:00
{
2017-08-25 17:55:26 +02:00
get { return 0 ; }
2013-11-07 17:16:22 +01:00
}
/// <summary>
/// Providers can override this setting, default is false to use better security
/// </summary>
2014-01-02 15:28:42 +11:00
public virtual bool DefaultUseLegacyEncoding
2013-11-07 17:16:22 +01:00
{
get { return false ; }
}
/// <summary>
2017-07-20 11:21:28 +02:00
/// Providers can override this setting, by default this is false which means that the provider will
2013-11-07 17:16:22 +01:00
/// authenticate the username + password when ChangePassword is called. This property exists purely for
/// backwards compatibility.
/// </summary>
2014-01-02 15:28:42 +11:00
public virtual bool AllowManuallyChangingPassword
2013-11-07 17:16:22 +01:00
{
get { return false ; }
}
2017-09-18 15:33:13 +02:00
/// <summary>
/// Returns the raw password value for a given user
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
/// <remarks>
/// By default this will return an invalid attempt, inheritors will need to override this to support it
/// </remarks>
protected virtual Attempt < string > GetRawPassword ( string username )
{
return Attempt < string > . Fail ( ) ;
}
2013-11-07 17:16:22 +01:00
private string _applicationName ;
private bool _enablePasswordReset ;
private bool _enablePasswordRetrieval ;
private int _maxInvalidPasswordAttempts ;
private int _minRequiredNonAlphanumericCharacters ;
private int _minRequiredPasswordLength ;
private int _passwordAttemptWindow ;
private MembershipPasswordFormat _passwordFormat ;
private string _passwordStrengthRegularExpression ;
private bool _requiresQuestionAndAnswer ;
private bool _requiresUniqueEmail ;
2014-01-02 15:28:42 +11:00
private string _customHashAlgorithmType ;
2017-05-12 14:49:44 +02:00
public bool UseLegacyEncoding { get ; private set ; }
2013-11-07 17:16:22 +01:00
#region Properties
/// <summary>
/// Indicates whether the membership provider is configured to allow users to reset their passwords.
/// </summary>
/// <value></value>
/// <returns>true if the membership provider supports password reset; otherwise, false. The default is true.</returns>
public override bool EnablePasswordReset
{
get { return _enablePasswordReset ; }
}
/// <summary>
/// Indicates whether the membership provider is configured to allow users to retrieve their passwords.
/// </summary>
/// <value></value>
/// <returns>true if the membership provider is configured to support password retrieval; otherwise, false. The default is false.</returns>
public override bool EnablePasswordRetrieval
{
get { return _enablePasswordRetrieval ; }
}
/// <summary>
/// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.
/// </summary>
/// <value></value>
/// <returns>The number of invalid password or password-answer attempts allowed before the membership user is locked out.</returns>
public override int MaxInvalidPasswordAttempts
{
get { return _maxInvalidPasswordAttempts ; }
}
/// <summary>
/// Gets the minimum number of special characters that must be present in a valid password.
/// </summary>
/// <value></value>
/// <returns>The minimum number of special characters that must be present in a valid password.</returns>
public override int MinRequiredNonAlphanumericCharacters
{
get { return _minRequiredNonAlphanumericCharacters ; }
}
/// <summary>
/// Gets the minimum length required for a password.
/// </summary>
/// <value></value>
/// <returns>The minimum length required for a password. </returns>
public override int MinRequiredPasswordLength
{
get { return _minRequiredPasswordLength ; }
}
/// <summary>
/// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
/// </summary>
/// <value></value>
/// <returns>The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.</returns>
public override int PasswordAttemptWindow
{
get { return _passwordAttemptWindow ; }
}
/// <summary>
/// Gets a value indicating the format for storing passwords in the membership data store.
/// </summary>
/// <value></value>
/// <returns>One of the <see cref="T:System.Web.Security.MembershipPasswordFormat"></see> values indicating the format for storing passwords in the data store.</returns>
public override MembershipPasswordFormat PasswordFormat
{
get { return _passwordFormat ; }
}
/// <summary>
/// Gets the regular expression used to evaluate a password.
/// </summary>
/// <value></value>
/// <returns>A regular expression used to evaluate a password.</returns>
public override string PasswordStrengthRegularExpression
{
get { return _passwordStrengthRegularExpression ; }
}
/// <summary>
/// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval.
/// </summary>
/// <value></value>
/// <returns>true if a password answer is required for password reset and retrieval; otherwise, false. The default is true.</returns>
public override bool RequiresQuestionAndAnswer
{
get { return _requiresQuestionAndAnswer ; }
}
/// <summary>
/// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.
/// </summary>
/// <value></value>
/// <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 _requiresUniqueEmail ; }
}
/// <summary>
/// The name of the application using the custom membership provider.
/// </summary>
/// <value></value>
/// <returns>The name of the application using the custom membership provider.</returns>
public override string ApplicationName
{
get
{
return _applicationName ;
}
set
{
if ( string . IsNullOrEmpty ( value ) )
throw new ProviderException ( "ApplicationName cannot be empty." ) ;
if ( value . Length > 0x100 )
throw new ProviderException ( "Provider application name too long." ) ;
_applicationName = value ;
}
}
#endregion
/// <summary>
/// Initializes the provider.
/// </summary>
/// <param name="name">The friendly name of the provider.</param>
/// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
/// <exception cref="T:System.ArgumentNullException">The name of the provider is null.</exception>
2017-07-20 11:21:28 +02:00
/// <exception cref="T:System.InvalidOperationException">An attempt is made to call
/// <see cref="M:System.Configuration.Provider.ProviderBase.Initialize(System.String,System.Collections.Specialized.NameValueCollection)"></see> on a provider after the provider
2013-11-07 17:16:22 +01:00
/// has already been initialized.</exception>
/// <exception cref="T:System.ArgumentException">The name of the provider has a length of zero.</exception>
public override void Initialize ( string name , NameValueCollection config )
2017-07-20 11:21:28 +02:00
{
2013-11-07 17:16:22 +01:00
// Initialize base provider class
base . Initialize ( name , config ) ;
_enablePasswordRetrieval = config . GetValue ( "enablePasswordRetrieval" , false ) ;
2017-08-25 17:55:26 +02:00
_enablePasswordReset = config . GetValue ( "enablePasswordReset" , true ) ;
2013-11-07 17:16:22 +01:00
_requiresQuestionAndAnswer = config . GetValue ( "requiresQuestionAndAnswer" , false ) ;
2014-01-02 15:28:42 +11:00
_requiresUniqueEmail = config . GetValue ( "requiresUniqueEmail" , true ) ;
2015-07-01 18:02:58 +02:00
_maxInvalidPasswordAttempts = GetIntValue ( config , "maxInvalidPasswordAttempts" , 5 , false , 0 ) ;
2013-11-07 17:16:22 +01:00
_passwordAttemptWindow = GetIntValue ( config , "passwordAttemptWindow" , 10 , false , 0 ) ;
_minRequiredPasswordLength = GetIntValue ( config , "minRequiredPasswordLength" , DefaultMinPasswordLength , true , 0x80 ) ;
_minRequiredNonAlphanumericCharacters = GetIntValue ( config , "minRequiredNonalphanumericCharacters" , DefaultMinNonAlphanumericChars , true , 0x80 ) ;
_passwordStrengthRegularExpression = config [ "passwordStrengthRegularExpression" ] ;
_applicationName = config [ "applicationName" ] ;
if ( string . IsNullOrEmpty ( _applicationName ) )
_applicationName = GetDefaultAppName ( ) ;
//by default we will continue using the legacy encoding.
2014-01-02 15:28:42 +11:00
UseLegacyEncoding = config . GetValue ( "useLegacyEncoding" , DefaultUseLegacyEncoding ) ;
2013-11-07 17:16:22 +01:00
2014-01-02 15:28:42 +11:00
// make sure password format is Hashed by default.
string str = config [ "passwordFormat" ] ? ? "Hashed" ;
2013-11-07 17:16:22 +01:00
switch ( str . ToLower ( ) )
{
case "clear" :
_passwordFormat = MembershipPasswordFormat . Clear ;
break ;
case "encrypted" :
_passwordFormat = MembershipPasswordFormat . Encrypted ;
break ;
case "hashed" :
_passwordFormat = MembershipPasswordFormat . Hashed ;
break ;
default :
throw new ProviderException ( "Provider bad password format" ) ;
}
if ( ( PasswordFormat = = MembershipPasswordFormat . Hashed ) & & EnablePasswordRetrieval )
2014-01-02 15:28:42 +11:00
{
var ex = new ProviderException ( "Provider can not retrieve a hashed password" ) ;
2016-09-11 19:57:33 +02:00
Current . Logger . Error < MembershipProviderBase > ( "Cannot specify a Hashed password format with the enabledPasswordRetrieval option set to true" , ex ) ;
2014-01-02 15:28:42 +11:00
throw ex ;
}
2017-07-20 11:21:28 +02:00
2014-01-02 15:28:42 +11:00
_customHashAlgorithmType = config . GetValue ( "hashAlgorithmType" , string . Empty ) ;
2013-11-07 17:16:22 +01:00
}
/// <summary>
/// Override this method to ensure the password is valid before raising the event
/// </summary>
/// <param name="e"></param>
protected override void OnValidatingPassword ( ValidatePasswordEventArgs e )
{
var attempt = IsPasswordValid ( e . Password , MinRequiredNonAlphanumericCharacters , PasswordStrengthRegularExpression , MinRequiredPasswordLength ) ;
if ( attempt . Success = = false )
{
e . Cancel = true ;
return ;
}
base . OnValidatingPassword ( e ) ;
}
protected internal enum PasswordValidityError
{
Ok ,
Length ,
AlphanumericChars ,
Strength
}
2017-07-20 11:21:28 +02:00
2013-11-07 17:16:22 +01:00
/// <summary>
2014-01-02 15:28:42 +11:00
/// Processes a request to update the password for a membership user.
2013-11-07 17:16:22 +01:00
/// </summary>
2014-01-02 15:28:42 +11:00
/// <param name="username">The user to update the password for.</param>
2017-09-18 15:33:13 +02:00
/// <param name="oldPassword">Required to change a user password if the user is not new and AllowManuallyChangingPassword is false</param>
2014-01-02 15:28:42 +11:00
/// <param name="newPassword">The new password for the specified user.</param>
/// <returns>
/// true if the password was updated successfully; otherwise, false.
/// </returns>
/// <remarks>
/// Checks to ensure the AllowManuallyChangingPassword rule is adhered to
2017-07-20 11:21:28 +02:00
/// </remarks>
2014-04-29 13:06:10 +10:00
public override bool ChangePassword ( string username , string oldPassword , string newPassword )
2013-11-07 17:16:22 +01:00
{
2017-09-18 15:33:13 +02:00
string rawPasswordValue = string . Empty ;
2013-11-07 17:16:22 +01:00
if ( oldPassword . IsNullOrWhiteSpace ( ) & & AllowManuallyChangingPassword = = false )
{
2017-09-18 15:33:13 +02:00
//we need to lookup the member since this could be a brand new member without a password set
var rawPassword = GetRawPassword ( username ) ;
rawPasswordValue = rawPassword . Success ? rawPassword . Result : string . Empty ;
if ( rawPassword . Success = = false | | rawPasswordValue . StartsWith ( Constants . Security . EmptyPasswordPrefix ) = = false )
{
//If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password
throw new NotSupportedException ( "This provider does not support manually changing the password" ) ;
}
2013-11-07 17:16:22 +01:00
}
2014-01-02 15:28:42 +11:00
var args = new ValidatePasswordEventArgs ( username , newPassword , false ) ;
OnValidatingPassword ( args ) ;
if ( args . Cancel )
{
if ( args . FailureInformation ! = null )
throw args . FailureInformation ;
throw new MembershipPasswordException ( "Change password canceled due to password validation failure." ) ;
}
2017-09-18 15:33:13 +02:00
//Special cases to allow changing password without validating existing credentials
// * the member is new and doesn't have a password set
// * during installation to set the admin password
2017-09-19 15:51:47 +02:00
var installing = Current . RuntimeState . Level = = RuntimeLevel . Install ;
2017-09-18 15:33:13 +02:00
if ( AllowManuallyChangingPassword = = false
& & ( rawPasswordValue . StartsWith ( Constants . Security . EmptyPasswordPrefix )
| | ( installing & & oldPassword = = "default" ) ) )
2017-05-12 14:49:44 +02:00
{
return PerformChangePassword ( username , oldPassword , newPassword ) ;
}
2014-01-02 15:28:42 +11:00
if ( AllowManuallyChangingPassword = = false )
{
if ( ValidateUser ( username , oldPassword ) = = false ) return false ;
}
2013-11-07 17:16:22 +01:00
return PerformChangePassword ( username , oldPassword , newPassword ) ;
}
2014-01-02 15:28:42 +11:00
/// <summary>
/// Processes a request to update the password for a membership user.
/// </summary>
/// <param name="username">The user to update the password for.</param>
/// <param name="oldPassword">This property is ignore for this provider</param>
/// <param name="newPassword">The new password for the specified user.</param>
/// <returns>
/// true if the password was updated successfully; otherwise, false.
/// </returns>
2013-11-07 17:16:22 +01:00
protected abstract bool PerformChangePassword ( string username , string oldPassword , string newPassword ) ;
2014-01-02 15:28:42 +11:00
/// <summary>
/// Processes a request to update the password question and answer for a membership user.
/// </summary>
/// <param name="username">The user to change the password question and answer for.</param>
/// <param name="password">The password for the specified user.</param>
/// <param name="newPasswordQuestion">The new password question for the specified user.</param>
/// <param name="newPasswordAnswer">The new password answer for the specified user.</param>
/// <returns>
/// true if the password question and answer are updated successfully; otherwise, false.
/// </returns>
/// <remarks>
/// Performs the basic validation before passing off to PerformChangePasswordQuestionAndAnswer
/// </remarks>
public override bool ChangePasswordQuestionAndAnswer ( string username , string password , string newPasswordQuestion , string newPasswordAnswer )
{
if ( RequiresQuestionAndAnswer = = false )
{
throw new NotSupportedException ( "Updating the password Question and Answer is not available if requiresQuestionAndAnswer is not set in web.config" ) ;
}
2017-07-20 11:21:28 +02:00
2014-01-02 15:28:42 +11:00
if ( AllowManuallyChangingPassword = = false )
{
if ( ValidateUser ( username , password ) = = false )
{
return false ;
}
}
return PerformChangePasswordQuestionAndAnswer ( username , password , newPasswordQuestion , newPasswordAnswer ) ;
}
/// <summary>
/// Processes a request to update the password question and answer for a membership user.
/// </summary>
/// <param name="username">The user to change the password question and answer for.</param>
/// <param name="password">The password for the specified user.</param>
/// <param name="newPasswordQuestion">The new password question for the specified user.</param>
/// <param name="newPasswordAnswer">The new password answer for the specified user.</param>
/// <returns>
/// true if the password question and answer are updated successfully; otherwise, false.
/// </returns>
protected abstract bool PerformChangePasswordQuestionAndAnswer ( string username , string password , string newPasswordQuestion , string newPasswordAnswer ) ;
/// <summary>
/// Adds a new membership user to the data source.
/// </summary>
/// <param name="username">The user name for the new user.</param>
/// <param name="password">The password for the new user.</param>
/// <param name="email">The e-mail address for the new user.</param>
/// <param name="passwordQuestion">The password question for the new user.</param>
/// <param name="passwordAnswer">The password answer for the new user</param>
/// <param name="isApproved">Whether or not the new user is approved to be validated.</param>
/// <param name="providerUserKey">The unique identifier from the membership data source for the user.</param>
/// <param name="status">A <see cref="T:System.Web.Security.MembershipCreateStatus"></see> enumeration value indicating whether the user was created successfully.</param>
/// <returns>
/// A <see cref="T:System.Web.Security.MembershipUser"></see> object populated with the information for the newly created user.
/// </returns>
/// <remarks>
/// Ensures the ValidatingPassword event is executed before executing PerformCreateUser and performs basic membership provider validation of values.
/// </remarks>
2014-04-29 13:06:10 +10:00
public override MembershipUser CreateUser ( string username , string password , string email , string passwordQuestion , string passwordAnswer , bool isApproved , object providerUserKey , out MembershipCreateStatus status )
2014-01-08 17:12:06 +11:00
{
var valStatus = ValidateNewUser ( username , password , email , passwordQuestion , passwordAnswer , isApproved , providerUserKey ) ;
if ( valStatus ! = MembershipCreateStatus . Success )
{
status = valStatus ;
return null ;
}
return PerformCreateUser ( username , password , email , passwordQuestion , passwordAnswer , isApproved , providerUserKey , out status ) ;
}
/// <summary>
/// Performs the validation of the information for creating a new user
/// </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>
/// <returns></returns>
protected MembershipCreateStatus ValidateNewUser ( string username , string password , string email , string passwordQuestion , string passwordAnswer , bool isApproved , object providerUserKey )
2014-01-02 15:28:42 +11:00
{
var args = new ValidatePasswordEventArgs ( username , password , true ) ;
OnValidatingPassword ( args ) ;
if ( args . Cancel )
{
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . InvalidPassword ;
2014-01-02 15:28:42 +11:00
}
// Validate password
var passwordValidAttempt = IsPasswordValid ( password , MinRequiredNonAlphanumericCharacters , PasswordStrengthRegularExpression , MinRequiredPasswordLength ) ;
if ( passwordValidAttempt . Success = = false )
{
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . InvalidPassword ;
2014-01-02 15:28:42 +11:00
}
// Validate email
if ( IsEmailValid ( email ) = = false )
{
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . InvalidEmail ;
2014-01-02 15:28:42 +11:00
}
// Make sure username isn't all whitespace
if ( string . IsNullOrWhiteSpace ( username . Trim ( ) ) )
{
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . InvalidUserName ;
2014-01-02 15:28:42 +11:00
}
// Check password question
if ( string . IsNullOrWhiteSpace ( passwordQuestion ) & & RequiresQuestionAndAnswer )
{
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . InvalidQuestion ;
2014-01-02 15:28:42 +11:00
}
// Check password answer
if ( string . IsNullOrWhiteSpace ( passwordAnswer ) & & RequiresQuestionAndAnswer )
{
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . InvalidAnswer ;
2014-01-02 15:28:42 +11:00
}
2014-01-08 17:12:06 +11:00
return MembershipCreateStatus . Success ;
2014-01-02 15:28:42 +11:00
}
/// <summary>
/// Adds a new membership user to the data source.
/// </summary>
/// <param name="username">The user name for the new user.</param>
/// <param name="password">The password for the new user.</param>
/// <param name="email">The e-mail address for the new user.</param>
/// <param name="passwordQuestion">The password question for the new user.</param>
/// <param name="passwordAnswer">The password answer for the new user</param>
/// <param name="isApproved">Whether or not the new user is approved to be validated.</param>
/// <param name="providerUserKey">The unique identifier from the membership data source for the user.</param>
/// <param name="status">A <see cref="T:System.Web.Security.MembershipCreateStatus"></see> enumeration value indicating whether the user was created successfully.</param>
/// <returns>
/// A <see cref="T:System.Web.Security.MembershipUser"></see> object populated with the information for the newly created user.
/// </returns>
protected abstract MembershipUser PerformCreateUser ( string username , string password , string email , string passwordQuestion , string passwordAnswer , bool isApproved , object providerUserKey , out MembershipCreateStatus status ) ;
/// <summary>
/// Gets the members password if password retreival is enabled
/// </summary>
/// <param name="username"></param>
/// <param name="answer"></param>
/// <returns></returns>
2014-04-29 13:06:10 +10:00
public override string GetPassword ( string username , string answer )
2014-01-02 15:28:42 +11:00
{
if ( EnablePasswordRetrieval = = false )
throw new ProviderException ( "Password Retrieval Not Enabled." ) ;
if ( PasswordFormat = = MembershipPasswordFormat . Hashed )
throw new ProviderException ( "Cannot retrieve Hashed passwords." ) ;
return PerformGetPassword ( username , answer ) ;
}
/// <summary>
/// Gets the members password if password retreival is enabled
/// </summary>
/// <param name="username"></param>
/// <param name="answer"></param>
/// <returns></returns>
protected abstract string PerformGetPassword ( string username , string answer ) ;
2014-04-29 13:06:10 +10:00
public override string ResetPassword ( string username , string answer )
2014-01-02 15:28:42 +11:00
{
2017-05-12 14:49:44 +02:00
var userService = Current . Services . UserService ;
2017-07-20 11:21:28 +02:00
var canReset = this . CanResetPassword ( userService ) ;
2017-05-12 14:49:44 +02:00
if ( canReset = = false )
2014-01-02 15:28:42 +11:00
{
throw new NotSupportedException ( "Password reset is not supported" ) ;
}
var newPassword = Membership . GeneratePassword ( MinRequiredPasswordLength , MinRequiredNonAlphanumericCharacters ) ;
var args = new ValidatePasswordEventArgs ( username , newPassword , true ) ;
OnValidatingPassword ( args ) ;
if ( args . Cancel )
{
if ( args . FailureInformation ! = null )
{
throw args . FailureInformation ;
}
throw new MembershipPasswordException ( "Reset password canceled due to password validation failure." ) ;
}
return PerformResetPassword ( username , answer , newPassword ) ;
}
protected abstract string PerformResetPassword ( string username , string answer , string generatedPassword ) ;
2013-11-07 17:16:22 +01:00
protected internal static Attempt < PasswordValidityError > IsPasswordValid ( string password , int minRequiredNonAlphanumericChars , string strengthRegex , int minLength )
{
if ( minRequiredNonAlphanumericChars > 0 )
{
var nonAlphaNumeric = Regex . Replace ( password , "[a-zA-Z0-9]" , "" , RegexOptions . Multiline | RegexOptions . IgnoreCase ) ;
if ( nonAlphaNumeric . Length < minRequiredNonAlphanumericChars )
{
return Attempt . Fail ( PasswordValidityError . AlphanumericChars ) ;
}
}
if ( string . IsNullOrEmpty ( strengthRegex ) = = false )
{
if ( Regex . IsMatch ( password , strengthRegex , RegexOptions . Compiled ) = = false )
{
return Attempt . Fail ( PasswordValidityError . Strength ) ;
}
2017-07-20 11:21:28 +02:00
2013-11-07 17:16:22 +01:00
}
if ( password . Length < minLength )
{
return Attempt . Fail ( PasswordValidityError . Length ) ;
}
return Attempt . Succeed ( PasswordValidityError . Ok ) ;
}
2017-07-20 11:21:28 +02:00
2013-11-07 17:16:22 +01:00
/// <summary>
/// Gets the name of the default app.
/// </summary>
/// <returns></returns>
internal static string GetDefaultAppName ( )
{
try
{
string applicationVirtualPath = HostingEnvironment . ApplicationVirtualPath ;
if ( string . IsNullOrEmpty ( applicationVirtualPath ) )
{
return "/" ;
}
return applicationVirtualPath ;
}
catch
{
return "/" ;
}
}
internal static int GetIntValue ( NameValueCollection config , string valueName , int defaultValue , bool zeroAllowed , int maxValueAllowed )
{
int num ;
string s = config [ valueName ] ;
if ( s = = null )
{
return defaultValue ;
}
if ( ! int . TryParse ( s , out num ) )
{
if ( zeroAllowed )
{
throw new ProviderException ( "Value must be non negative integer" ) ;
}
throw new ProviderException ( "Value must be positive integer" ) ;
}
if ( zeroAllowed & & ( num < 0 ) )
{
throw new ProviderException ( "Value must be non negativeinteger" ) ;
}
if ( ! zeroAllowed & & ( num < = 0 ) )
{
throw new ProviderException ( "Value must be positive integer" ) ;
}
if ( ( maxValueAllowed > 0 ) & & ( num > maxValueAllowed ) )
{
throw new ProviderException ( "Value too big" ) ;
}
return num ;
}
2014-01-02 15:28:42 +11:00
/// <summary>
/// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password
/// to the hashed password itself.
/// </summary>
/// <param name="pass"></param>
/// <param name="salt"></param>
/// <returns></returns>
protected internal string FormatPasswordForStorage ( string pass , string salt )
2013-11-07 17:16:22 +01:00
{
2014-01-02 15:28:42 +11:00
if ( UseLegacyEncoding )
2013-11-07 17:16:22 +01:00
{
return pass ;
}
2017-07-20 11:21:28 +02:00
2014-01-02 15:28:42 +11:00
if ( PasswordFormat = = MembershipPasswordFormat . Hashed )
{
//the better way, we use salt per member
return salt + pass ;
}
return pass ;
}
internal static bool IsEmailValid ( string email )
{
const string pattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
+ @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?<!\.)\.)*)(?<!\.)"
+ @"@[a-z0-9][\w\.-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$" ;
2013-11-07 17:16:22 +01:00
2014-01-02 15:28:42 +11:00
return Regex . IsMatch ( email , pattern , RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
2013-11-07 17:16:22 +01:00
}
2014-01-02 15:28:42 +11:00
protected internal string EncryptOrHashPassword ( string pass , string salt )
2013-11-07 17:16:22 +01:00
{
//if we are doing it the old way
2014-01-02 15:28:42 +11:00
if ( UseLegacyEncoding )
2013-11-07 17:16:22 +01:00
{
return LegacyEncodePassword ( pass ) ;
}
//This is the correct way to implement this (as per the sql membership provider)
2014-01-02 15:28:42 +11:00
if ( PasswordFormat = = MembershipPasswordFormat . Clear )
2013-11-07 17:16:22 +01:00
return pass ;
var bytes = Encoding . Unicode . GetBytes ( pass ) ;
2017-08-25 17:55:26 +02:00
var saltBytes = Convert . FromBase64String ( salt ) ;
2013-11-07 17:16:22 +01:00
byte [ ] inArray ;
2014-01-02 15:28:42 +11:00
if ( PasswordFormat = = MembershipPasswordFormat . Hashed )
2013-11-07 17:16:22 +01:00
{
var hashAlgorithm = GetHashAlgorithm ( pass ) ;
var algorithm = hashAlgorithm as KeyedHashAlgorithm ;
if ( algorithm ! = null )
{
var keyedHashAlgorithm = algorithm ;
2017-08-25 17:55:26 +02:00
if ( keyedHashAlgorithm . Key . Length = = saltBytes . Length )
2017-09-18 15:33:13 +02:00
{
2017-08-25 17:55:26 +02:00
//if the salt bytes is the required key length for the algorithm, use it as-is
keyedHashAlgorithm . Key = saltBytes ;
}
else if ( keyedHashAlgorithm . Key . Length < saltBytes . Length )
2017-09-18 15:33:13 +02:00
{
2017-08-25 17:55:26 +02:00
//if the salt bytes is too long for the required key length for the algorithm, reduce it
2013-11-07 17:16:22 +01:00
var numArray2 = new byte [ keyedHashAlgorithm . Key . Length ] ;
2017-08-25 17:55:26 +02:00
Buffer . BlockCopy ( saltBytes , 0 , numArray2 , 0 , numArray2 . Length ) ;
2013-11-07 17:16:22 +01:00
keyedHashAlgorithm . Key = numArray2 ;
}
else
{
2017-08-25 17:55:26 +02:00
//if the salt bytes is too long for the required key length for the algorithm, extend it
2013-11-07 17:16:22 +01:00
var numArray2 = new byte [ keyedHashAlgorithm . Key . Length ] ;
var dstOffset = 0 ;
while ( dstOffset < numArray2 . Length )
{
2017-08-25 17:55:26 +02:00
var count = Math . Min ( saltBytes . Length , numArray2 . Length - dstOffset ) ;
Buffer . BlockCopy ( saltBytes , 0 , numArray2 , dstOffset , count ) ;
2013-11-07 17:16:22 +01:00
dstOffset + = count ;
}
keyedHashAlgorithm . Key = numArray2 ;
}
inArray = keyedHashAlgorithm . ComputeHash ( bytes ) ;
}
else
{
2017-08-25 17:55:26 +02:00
var buffer = new byte [ saltBytes . Length + bytes . Length ] ;
Buffer . BlockCopy ( saltBytes , 0 , buffer , 0 , saltBytes . Length ) ;
Buffer . BlockCopy ( bytes , 0 , buffer , saltBytes . Length , bytes . Length ) ;
2013-11-07 17:16:22 +01:00
inArray = hashAlgorithm . ComputeHash ( buffer ) ;
}
}
else
{
2014-01-02 15:28:42 +11:00
//this code is copied from the sql membership provider - pretty sure this could be nicely re-written to completely
// ignore the salt stuff since we are not salting the password when encrypting.
2017-08-25 17:55:26 +02:00
var password = new byte [ saltBytes . Length + bytes . Length ] ;
Buffer . BlockCopy ( saltBytes , 0 , password , 0 , saltBytes . Length ) ;
Buffer . BlockCopy ( bytes , 0 , password , saltBytes . Length , bytes . Length ) ;
2013-11-07 17:16:22 +01:00
inArray = EncryptPassword ( password , MembershipPasswordCompatibilityMode . Framework40 ) ;
}
return Convert . ToBase64String ( inArray ) ;
}
/// <summary>
2014-01-02 15:28:42 +11:00
/// Checks the password.
2013-11-07 17:16:22 +01:00
/// </summary>
2014-01-02 15:28:42 +11:00
/// <param name="password">The password.</param>
/// <param name="dbPassword">The dbPassword.</param>
2013-11-07 17:16:22 +01:00
/// <returns></returns>
2014-01-02 15:28:42 +11:00
protected internal bool CheckPassword ( string password , string dbPassword )
2013-11-07 17:16:22 +01:00
{
2017-08-25 17:55:26 +02:00
if ( string . IsNullOrWhiteSpace ( dbPassword ) ) throw new ArgumentException ( "Value cannot be null or whitespace." , "dbPassword" ) ;
2014-01-02 15:28:42 +11:00
switch ( PasswordFormat )
{
case MembershipPasswordFormat . Encrypted :
var decrypted = DecryptPassword ( dbPassword ) ;
return decrypted = = password ;
case MembershipPasswordFormat . Hashed :
string salt ;
var storedHashedPass = StoredPassword ( dbPassword , out salt ) ;
var hashed = EncryptOrHashPassword ( password , salt ) ;
return storedHashedPass = = hashed ;
case MembershipPasswordFormat . Clear :
return password = = dbPassword ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
2013-11-07 17:16:22 +01:00
}
/// <summary>
2014-01-02 15:28:42 +11:00
/// Encrypt/hash a new password with a new salt
2013-11-07 17:16:22 +01:00
/// </summary>
2014-01-02 15:28:42 +11:00
/// <param name="newPassword"></param>
/// <param name="salt"></param>
2013-11-07 17:16:22 +01:00
/// <returns></returns>
2014-01-02 15:28:42 +11:00
protected internal string EncryptOrHashNewPassword ( string newPassword , out string salt )
2013-11-07 17:16:22 +01:00
{
2014-01-02 15:28:42 +11:00
salt = GenerateSalt ( ) ;
return EncryptOrHashPassword ( newPassword , salt ) ;
2013-11-07 17:16:22 +01:00
}
2014-01-02 15:28:42 +11:00
protected internal string DecryptPassword ( string pass )
2013-11-07 17:16:22 +01:00
{
//if we are doing it the old way
2014-01-02 15:28:42 +11:00
if ( UseLegacyEncoding )
2013-11-07 17:16:22 +01:00
{
return LegacyUnEncodePassword ( pass ) ;
}
//This is the correct way to implement this (as per the sql membership provider)
2016-02-05 03:29:33 +11:00
switch ( PasswordFormat )
2013-11-07 17:16:22 +01:00
{
2016-02-05 03:29:33 +11:00
case MembershipPasswordFormat . Clear :
2013-11-07 17:16:22 +01:00
return pass ;
2016-02-05 03:29:33 +11:00
case MembershipPasswordFormat . Hashed :
2014-01-02 15:28:42 +11:00
throw new ProviderException ( "Provider can not decrypt hashed password" ) ;
2016-02-05 03:29:33 +11:00
case MembershipPasswordFormat . Encrypted :
2013-11-07 17:16:22 +01:00
default :
var bytes = DecryptPassword ( Convert . FromBase64String ( pass ) ) ;
return bytes = = null ? null : Encoding . Unicode . GetString ( bytes , 16 , bytes . Length - 16 ) ;
}
}
/// <summary>
/// Returns the hashed password without the salt if it is hashed
/// </summary>
/// <param name="storedString"></param>
/// <param name="salt">returns the salt</param>
/// <returns></returns>
2014-01-02 15:28:42 +11:00
internal string StoredPassword ( string storedString , out string salt )
2013-11-07 17:16:22 +01:00
{
2017-08-25 17:55:26 +02:00
if ( string . IsNullOrWhiteSpace ( storedString ) ) throw new ArgumentException ( "Value cannot be null or whitespace." , "storedString" ) ;
2014-01-02 15:28:42 +11:00
if ( UseLegacyEncoding )
{
salt = string . Empty ;
return storedString ;
}
switch ( PasswordFormat )
2013-11-07 17:16:22 +01:00
{
case MembershipPasswordFormat . Hashed :
var saltLen = GenerateSalt ( ) ;
salt = storedString . Substring ( 0 , saltLen . Length ) ;
2017-07-20 11:21:28 +02:00
return storedString . Substring ( saltLen . Length ) ;
case MembershipPasswordFormat . Clear :
2013-11-07 17:16:22 +01:00
case MembershipPasswordFormat . Encrypted :
default :
salt = string . Empty ;
return storedString ;
2017-07-20 11:21:28 +02:00
2013-11-07 17:16:22 +01:00
}
}
protected internal static string GenerateSalt ( )
{
var numArray = new byte [ 16 ] ;
new RNGCryptoServiceProvider ( ) . GetBytes ( numArray ) ;
return Convert . ToBase64String ( numArray ) ;
}
2014-01-02 15:28:42 +11:00
protected internal HashAlgorithm GetHashAlgorithm ( string password )
2013-11-07 17:16:22 +01:00
{
2014-01-02 15:28:42 +11:00
if ( UseLegacyEncoding )
2013-11-07 17:16:22 +01:00
{
//before we were never checking for an algorithm type so we were always using HMACSHA1
// for any SHA specified algorithm :( so we'll need to keep doing that for backwards compat support.
if ( Membership . HashAlgorithmType . InvariantContains ( "SHA" ) )
{
return new HMACSHA1
{
//the legacy salt was actually the password :(
Key = Encoding . Unicode . GetBytes ( password )
} ;
2017-07-20 11:21:28 +02:00
}
2013-11-07 17:16:22 +01:00
}
2017-07-20 11:21:28 +02:00
2013-11-07 17:16:22 +01:00
//get the algorithm by name
2014-01-02 15:28:42 +11:00
if ( _customHashAlgorithmType . IsNullOrWhiteSpace ( ) )
{
_customHashAlgorithmType = Membership . HashAlgorithmType ;
}
var alg = HashAlgorithm . Create ( _customHashAlgorithmType ) ;
if ( alg = = null )
{
throw new InvalidOperationException ( "The hash algorithm specified " + Membership . HashAlgorithmType + " cannot be resolved" ) ;
}
return alg ;
2013-11-07 17:16:22 +01:00
}
/// <summary>
/// Encodes the password.
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The encoded password.</returns>
protected string LegacyEncodePassword ( string password )
{
string encodedPassword = password ;
switch ( PasswordFormat )
{
case MembershipPasswordFormat . Clear :
break ;
case MembershipPasswordFormat . Encrypted :
encodedPassword =
Convert . ToBase64String ( EncryptPassword ( Encoding . Unicode . GetBytes ( password ) ) ) ;
break ;
case MembershipPasswordFormat . Hashed :
var hashAlgorith = GetHashAlgorithm ( password ) ;
encodedPassword = Convert . ToBase64String ( hashAlgorith . ComputeHash ( Encoding . Unicode . GetBytes ( password ) ) ) ;
break ;
default :
throw new ProviderException ( "Unsupported password format." ) ;
}
return encodedPassword ;
}
/// <summary>
/// Unencode password.
/// </summary>
/// <param name="encodedPassword">The encoded password.</param>
/// <returns>The unencoded password.</returns>
protected string LegacyUnEncodePassword ( 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 :
throw new ProviderException ( "Cannot unencode a hashed password." ) ;
default :
throw new ProviderException ( "Unsupported password format." ) ;
}
return password ;
}
2014-01-02 15:28:42 +11:00
public override string ToString ( )
{
var result = base . ToString ( ) ;
var sb = new StringBuilder ( result ) ;
sb . AppendLine ( "Name =" + Name ) ;
sb . AppendLine ( "_applicationName =" + _applicationName ) ;
sb . AppendLine ( "_enablePasswordReset=" + _enablePasswordReset ) ;
sb . AppendLine ( "_enablePasswordRetrieval=" + _enablePasswordRetrieval ) ;
sb . AppendLine ( "_maxInvalidPasswordAttempts=" + _maxInvalidPasswordAttempts ) ;
sb . AppendLine ( "_minRequiredNonAlphanumericCharacters=" + _minRequiredNonAlphanumericCharacters ) ;
sb . AppendLine ( "_minRequiredPasswordLength=" + _minRequiredPasswordLength ) ;
sb . AppendLine ( "_passwordAttemptWindow=" + _passwordAttemptWindow ) ;
sb . AppendLine ( "_passwordFormat=" + _passwordFormat ) ;
sb . AppendLine ( "_passwordStrengthRegularExpression=" + _passwordStrengthRegularExpression ) ;
sb . AppendLine ( "_requiresQuestionAndAnswer=" + _requiresQuestionAndAnswer ) ;
sb . AppendLine ( "_requiresUniqueEmail=" + _requiresUniqueEmail ) ;
return sb . ToString ( ) ;
}
2015-05-11 12:22:56 +10:00
/// <summary>
/// Returns the current request IP address for logging if there is one
/// </summary>
/// <returns></returns>
protected string GetCurrentRequestIpAddress ( )
{
var httpContext = HttpContext . Current = = null ? ( HttpContextBase ) null : new HttpContextWrapper ( HttpContext . Current ) ;
return httpContext . GetCurrentRequestIpAddress ( ) ;
}
2013-11-07 17:16:22 +01:00
}
2017-07-20 11:21:28 +02:00
}