- Download source files and setup package from Part 1
Article Series
This article is the last part of a series on developing a Silverlight business application using MEF, MVVM Light, and WCF RIA Services.
Contents
Introduction
In this last part, we will discuss how custom authentication, reset password, and user maintenance are implemented in this sample application.
First, let's reiterate the main features we will discuss:
- There are two types of user accounts, Admin user accounts and normal user accounts.
- Only Admin users can add, delete, or update users through the User Maintenance screen.
- Normal users have no access to the User Maintenance screen, and can only update their own profile.
- After an account is added or updated, users will be prompted to reset the password and security answer when they first login.
- If a user forgets password, the reset password screen can be used to create a new password based on the security answer.
- If a user forgets both password and security answer, then only the Admin user can reset the password.
User, LoginUser, and PasswordResetUser
User
, LoginUser
and PasswordResetUser
are three classes defined in the project IssueVision.Data.Web.
The User
class is an EntityObject
class from the IssueVision
Entity Model. Because the User
class
is defined as a partial class, we can add a few new properties as follows:
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
internal class UserMetadata
{
protected UserMetadata()
{
}
[Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^[a-zA-Z0-9_]*$",
ErrorMessageResourceName = "ValidationErrorInvalidUserName",
ErrorMessageResourceType = typeof(ErrorResources))]
public string Name { get; set; }
[CustomValidation(typeof(UserRules), "IsValidEmail")]
public string Email { get; set; }
[Exclude]
public string PasswordAnswerHash { get; set; }
[Exclude]
public string PasswordAnswerSalt { get; set; }
[Exclude]
public string PasswordHash { get; set; }
[Exclude]
public string PasswordSalt { get; set; }
[Exclude]
public Byte ProfileReset { get; set; }
}
[DataMember]
[Display(Name = "PasswordLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^.*[^a-zA-Z0-9].*$",
ErrorMessageResourceName = "ValidationErrorBadPasswordStrength",
ErrorMessageResourceType = typeof(ErrorResources))]
[StringLength(50, MinimumLength = 12,
ErrorMessageResourceName = "ValidationErrorBadPasswordLength",
ErrorMessageResourceType = typeof(ErrorResources))]
public string Password { get; set; }
[DataMember]
[Display(Name = "NewPasswordLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^.*[^a-zA-Z0-9].*$",
ErrorMessageResourceName = "ValidationErrorBadPasswordStrength",
ErrorMessageResourceType = typeof(ErrorResources))]
[StringLength(50, MinimumLength = 12,
ErrorMessageResourceName = "ValidationErrorBadPasswordLength",
ErrorMessageResourceType = typeof(ErrorResources))]
public string NewPassword { get; set; }
[DataMember]
[Display(Name = "SecurityAnswerLabel",
ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
public string PasswordAnswer { get; set; }
[DataMember]
public bool IsUserMaintenance { get; set; }
[DataMember]
public bool ProfileResetFlag
{
get
{
return this.ProfileReset != (byte)0;
}
}
}
From the code above, you can see that we have added some attributes through the UserMetadata
class. Specifically, we excluded the properties
PasswordAnswerHash
, PasswordAnswerSalt
, PasswordHash
, PasswordSalt
, and ProfileReset
from being auto-generated
on the client side. In addition, we have added the new properties Password
, NewPassword
, PasswordAnswer
, and a read-only property
ProfileResetFlag
. These changes ensure that any password hash and password salt values only stay on the server side and never transfer through the wire.
The User
class is used by the screens MyProfile
and UserMaintenance
, and we will go over that topic later. For now,
let's examine the LoginUser
and PasswordResetUser
classes.
The LoginUser
class is a sub-class of the User
class, and implements the interface IUser
. It is used within the
class AuthenticationService
. Following is its definition:
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(LoginUser.LoginUserMetadata))]
public sealed class LoginUser : User, IUser
{
internal sealed class LoginUserMetadata : UserMetadata
{
private LoginUserMetadata()
{
}
[Key]
[Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^[a-zA-Z0-9_]*$",
ErrorMessageResourceName = "ValidationErrorInvalidUserName",
ErrorMessageResourceType = typeof(ErrorResources))]
public new string Name { get; set; }
[Exclude]
public new string Email { get; set; }
[Exclude]
public string FirstName { get; set; }
[Exclude]
public string LastName { get; set; }
[Exclude]
public string NewPassword { get; set; }
[Exclude]
public string PasswordQuestion { get; set; }
[Exclude]
public string PasswordAnswer { get; set; }
[Exclude]
public string UserType { get; set; }
[Exclude]
public bool IsUserMaintenance { get; set; }
}
[DataMember]
public IEnumerable<string> Roles
{
get
{
switch (UserType)
{
case "A":
return new List<string> {
IssueVisionServiceConstant.UserTypeUser,
IssueVisionServiceConstant.UserTypeAdmin };
case "U":
return new List<string> { "User" };
default:
return new List<string>();
}
}
set
{
if (value.Contains(IssueVisionServiceConstant.UserTypeAdmin))
{
UserType = "A";
}
else if (value.Contains(IssueVisionServiceConstant.UserTypeUser))
{
UserType = "U";
}
else
UserType = String.Empty;
}
}
}
Like in the User
class, we excluded from the LoginUser
class all properties from being auto-generated to the client side, except four
properties: Name
, Roles
, Password
, and ProfileResetFlag
. The first two are required by the interface
IUser
, and the last property ProfileResetFlag
is used to determine whether we need to ask the user to reset the profile after the account
is newly created or recently updated by the Admin user.
Next, let's take a look at the PasswordResetUser
class. This class is also a sub-class of User
, and is used by the class
PasswordResetService
. It only exposes four properties: Name
, NewPassword
, PasswordQuestion
, and
PasswordAnswer
, and is defined as follows:
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(PasswordResetUser.PasswordResetUserMetadata))]
public sealed class PasswordResetUser : User
{
internal sealed class PasswordResetUserMetadata : UserMetadata
{
private PasswordResetUserMetadata()
{
}
[Key]
[Display(Name = "UserNameLabel", ResourceType = typeof(IssueVisionResources))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField",
ErrorMessageResourceType = typeof(ErrorResources))]
[RegularExpression("^[a-zA-Z0-9_]*$",
ErrorMessageResourceName = "ValidationErrorInvalidUserName",
ErrorMessageResourceType = typeof(ErrorResources))]
public new string Name { get; set; }
[DataMember]
[Display(Name = "SecurityQuestionLabel",
ResourceType = typeof(IssueVisionResources))]
public string PasswordQuestion { get; set; }
[Exclude]
public new string Email { get; set; }
[Exclude]
public string FirstName { get; set; }
[Exclude]
public string LastName { get; set; }
[Exclude]
public string Password { get; set; }
[Exclude]
public string UserType { get; set; }
[Exclude]
public bool IsUserMaintenance { get; set; }
[Exclude]
public bool ProfileResetFlag { get; set; }
}
}
As we now know how the User
, LoginUser
, and PasswordResetUser
classes are defined, we are ready to see how they
are actually being used inside the AuthenticationService
and PasswordResetService
classes.
AuthenticationService
AuthenticationService
is a DomainService
class that implements the interface IAuthentication<LoginUser>
, and it
is the class providing custom authentication. Here is how the main function login()
gets implemented:
public LoginUser Login(string userName, string password,
bool isPersistent, string customData)
{
try
{
string userData;
if (ValidateUser(userName, password, out userData))
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
userName,
DateTime.Now, DateTime.Now.AddDays(7),
isPersistent,
userData,
FormsAuthentication.FormsCookiePath);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie authCookie = new HttpCookie(
FormsAuthentication.FormsCookieName, encryptedTicket);
if (ticket.IsPersistent)
{
authCookie.Expires = ticket.Expiration;
}
HttpContextBase httpContext =
(HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.Response.Cookies.Add(authCookie);
return GetUserByName(userName);
}
return DefaultUser;
}
catch (Exception ex)
{
Exception actualException = ex;
while (actualException.InnerException != null)
{
actualException = actualException.InnerException;
}
throw actualException;
}
private bool ValidateUser(string username, string password,
out string userData)
{
userData = null;
LoginUser foundUser = GetUserByName(username);
if (foundUser != null)
{
string passwordHash =
HashHelper.ComputeSaltedHash(password, foundUser.PasswordSalt);
if (string.Equals(passwordHash, foundUser.PasswordHash,
StringComparison.Ordinal))
{
userData = foundUser.UserType;
return true;
}
return false;
}
return false;
}
The Login()
function calls a private function ValidateUser()
, and ValidateUser()
will generate a
hash value based on the password the user supplied and the password salt saved in the database. If the hash value matches what is stored in the database, the user is authenticated.
PasswordResetService
Similarly, PasswordResetService
is also a DomainService
class. It has only two functions. The first function
GetUserByName()
accepts a user name as the only parameter, and returns back a valid PasswordResetUser
object if the user name
exists in the database. This function is called by the login screen to find out the security question before switching to the reset-password screen.
The second function is UpdateUser()
. This function takes a PasswordResetUser
object from the client, and checks whether the
security question and answer match what is stored in the database. If they match, the new password is saved into the database as a pair of password salt and password hash.
[Update]
public void UpdateUser(PasswordResetUser passwordResetUser)
{
User foundUser = ObjectContext.Users.FirstOrDefault(
u => u.Name == passwordResetUser.Name);
if (foundUser != null)
{
string passwordAnswerHash = HashHelper.ComputeSaltedHash(
passwordResetUser.PasswordAnswer, foundUser.PasswordAnswerSalt);
if ((string.Equals(passwordResetUser.PasswordQuestion,
foundUser.PasswordQuestion, StringComparison.Ordinal)) &&
(string.Equals(passwordAnswerHash, foundUser.PasswordAnswerHash,
StringComparison.Ordinal)))
{
foundUser.PasswordSalt = HashHelper.CreateRandomSalt();
foundUser.PasswordHash = HashHelper.ComputeSaltedHash(
passwordResetUser.NewPassword, foundUser.PasswordSalt);
foundUser.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
foundUser.PasswordAnswerHash =
HashHelper.ComputeSaltedHash(passwordResetUser.PasswordAnswer,
foundUser.PasswordAnswerSalt);
}
else
throw new UnauthorizedAccessException(
ErrorResources.PasswordQuestionDoesNotMatch);
}
else
throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
}
So far, we have finished examining the server-side data access layer logic for custom authentication and reset password. We will switch to the client side next.
AuthenticationModel and PasswordResetModel
From the client side, the LoginForm.xaml screen binds to its ViewModel class LoginFormViewModel
during runtime, and the
ViewModel class has a reference to objects of AuthenticationModel
and PasswordResetModel
, which we will discuss now.
The AuthenticationModel
class is based on the interface IAuthenticationModel
defined below:
public interface IAuthenticationModel : INotifyPropertyChanged
{
void LoadUserAsync();
event EventHandler<LoadUserOperationEventArgs> LoadUserComplete;
void LoginAsync(LoginParameters loginParameters);
event EventHandler<LoginOperationEventArgs> LoginComplete;
void LogoutAsync();
event EventHandler<LogoutOperationEventArgs> LogoutComplete;
IPrincipal User { get; }
Boolean IsBusy { get; }
Boolean IsLoadingUser { get; }
Boolean IsLoggingIn { get; }
Boolean IsLoggingOut { get; }
Boolean IsSavingUser { get; }
event EventHandler<AuthenticationEventArgs> AuthenticationChanged;
}
And following is the implementation of its main function LoginAsync()
:
public void LoginAsync(LoginParameters loginParameters)
{
AuthService.Login(loginParameters, LoginOperation_Completed, null);
}
The Login()
function inside LoginAsync()
will eventually call the server-side Login()
function from the
AuthenticationService
class we discussed above.
Likewise, PasswordResetModel
is based on the interface IPasswordResetModel
.
public interface IPasswordResetModel : INotifyPropertyChanged
{
void GetUserByNameAsync(string name);
event EventHandler<EntityResultsArgs<PasswordResetUser>> GetUserComplete;
void SaveUserAsync();
event EventHandler<ResultsArgs> SaveUserComplete;
void RejectChanges();
Boolean IsBusy { get; }
}
The function GetUserByNameAsync()
gets called by the ViewModel class LoginFormViewModel
when it needs to find out the right
security question before switching to the reset-password screen. SaveUserAsync()
is used inside ResetPasswordCommand
,
and it eventually calls the server-side UpdateUser()
from the PasswordResetService
class to verify and save a new password if
both the security question and answer match what is in the database.
This concludes our discussion about custom authentication and reset password logic. Next, let's look into how user maintenance is done.
My Profile Screen
As we stated above, the My Profile screen uses the User
class. This screen binds to the ViewModel class MyProfileViewModel
, which
retrieves and updates user information through two server-side functions GetCurrentUser()
and UpdateUser()
from the IssueVisionService
class.
Also, during the first successful login after an account has been updated or added by the Admin user, the My Profile screen will be shown instead of the Home page:
The actual logic to implement this resides in the ViewModel class MainPageViewModel
, and is as follows:
private void _authenticationModel_AuthenticationChanged(object sender,
AuthenticationEventArgs e)
{
IsLoggedIn = e.User.Identity.IsAuthenticated;
IsLoggedOut = !(e.User.Identity.IsAuthenticated);
IsAdmin = e.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin);
if (e.User.Identity.IsAuthenticated)
{
WelcomeText = "Welcome " + e.User.Identity.Name;
if (e.User is LoginUser)
{
if (((LoginUser)e.User).ProfileResetFlag)
{
AppMessages.ChangeScreenMessage.Send(ViewTypes.MyProfileView);
CurrentScreenText = ViewTypes.MyProfileView;
}
else
{
AppMessages.ChangeScreenMessage.Send(ViewTypes.HomeView);
CurrentScreenText = ViewTypes.HomeView;
}
}
}
else
WelcomeText = string.Empty;
}
User Maintenance Screen
Lastly, we will talk about the User Maintenance screen. This screen is only available to Admin users. It binds to the ViewModel class
UserMaintenanceViewModel
, and eventually retrieves and updates user information through the functions GetUsers()
,
InsertUser()
, UpdateUser()
, and DeleteUser()
from the IssueVisionService
class on the
server side. Let's check how the function InsertUser()
is implemented:
public void InsertUser(User user)
{
if (CheckUserInsertPermission(user) && user.IsUserMaintenance)
{
User foundUser = ObjectContext.Users.Where(
n => n.Name == user.Name).FirstOrDefault();
if (foundUser != null)
throw new ValidationException(ErrorResources.CannotInsertDuplicateUser);
user.PasswordSalt = HashHelper.CreateRandomSalt();
user.PasswordHash = HashHelper.ComputeSaltedHash(
user.NewPassword, user.PasswordSalt);
SecurityQuestion securityQuestion =
ObjectContext.SecurityQuestions.FirstOrDefault();
if (securityQuestion != null)
user.PasswordQuestion = securityQuestion.PasswordQuestion;
user.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
user.PasswordAnswerHash = HashHelper.CreateRandomSalt();
user.ProfileReset = 1;
if ((user.EntityState != EntityState.Detached))
{
ObjectContext.ObjectStateManager.ChangeObjectState(
user, EntityState.Added);
}
else
{
ObjectContext.Users.AddObject(user);
}
}
else
throw new ValidationException(ErrorResources.NoPermissionToInsertUser);
}
From the code above, we can see that no security answer is actually set when a new user is first created. This is one of the reasons that users are reminded
to reset their profile during the first login.
Further Work
This concludes our discussion. There is, of course, further work needed to improve this sample application. One of the obvious and (intentional) omissions
is the unit test project. Also, adding a logging mechanism will help trace any potential problems.
I hope you find this article series useful, and please rate and/or leave feedback below. Thank you!
References
History
- May 2010 - Initial release
- July 2010 - Minor update based on feedback
- November 2010 - Update to support VS2010 Express Edition
- February 2011 - Update to fix multiple bugs including memory leak issues
- July 2011 - Update to fix multiple bugs