Click here to Skip to main content
Click here to Skip to main content

A Sample Silverlight 4 Application Using MEF, MVVM, and WCF RIA Services - Part 3

By , 8 Jul 2011
Rate this:
Please Sign up or sign in to vote.
  • 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:

/// <summary>
/// User class exposes the following data members to the client:
/// Name, FirstName, LastName, Email, Password, NewPassword,
/// PasswordQuestion, PasswordAnswer, UserType, IsUserMaintenance
/// and ProfileResetFlag
/// </summary>
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
    internal class UserMetadata
    {
        // Metadata classes are not meant to be instantiated.
        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:

/// <summary>
/// LoginUser class derives from User class and implements IUser interface,
/// it only exposes the following three data members to the client:
/// Name, Password, ProfileResetFlag, and Roles
/// </summary>
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(LoginUser.LoginUserMetadata))]
public sealed class LoginUser : User, IUser
{
    internal sealed class LoginUserMetadata : UserMetadata
    {
        // Metadata classes are not meant to be instantiated.
        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))
        {
          // Admin User
          UserType = "A";
        }
        else if (value.Contains(IssueVisionServiceConstant.UserTypeUser))
        {
          // Normal User
          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:

/// <summary>
/// PasswordRestUser derives from User class and
/// only exposes the following four data members to the client:
/// Name, NewPassword, PasswordQuestion, and PasswordAnswer
/// </summary>
[DataContractAttribute(IsReference = true)]
[MetadataTypeAttribute(typeof(PasswordResetUser.PasswordResetUserMetadata))]
public sealed class PasswordResetUser : User
{
    internal sealed class PasswordResetUserMetadata : UserMetadata
    {
        // Metadata classes are not meant to be instantiated.
        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:

/// <summary>
/// Validate and login
/// </summary>
public LoginUser Login(string userName, string password, 
                       bool isPersistent, string customData)
{
    try
    {
        string userData;

        if (ValidateUser(userName, password, out userData))
        {
            // if IsPersistent is true, will keep logged in for up to a week 
            // (or until you logout)
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
                /* version */ 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;
    }

/// <summary>
/// Validate user with password
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="userData"></param>
/// <returns></returns>
private bool ValidateUser(string username, string password, 
                          out string userData)
{
    userData = null;

    LoginUser foundUser = GetUserByName(username);

    if (foundUser != null)
    {
        // generate password hash
        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.

/// <summary>
/// Update user information to the database
/// User information can only be updated if the user
/// question/answer matches.
/// </summary>
[Update]
public void UpdateUser(PasswordResetUser passwordResetUser)
{
    // Search user from database by name
    User foundUser = ObjectContext.Users.FirstOrDefault(
                       u => u.Name == passwordResetUser.Name);

    if (foundUser != null)
    {
        // generate password answer hash
        string passwordAnswerHash = HashHelper.ComputeSaltedHash(
          passwordResetUser.PasswordAnswer, foundUser.PasswordAnswerSalt);

        if ((string.Equals(passwordResetUser.PasswordQuestion, 
             foundUser.PasswordQuestion, StringComparison.Ordinal)) &&
             (string.Equals(passwordAnswerHash, foundUser.PasswordAnswerHash, 
              StringComparison.Ordinal)))
        {
            // Password answer matches, so save the new user password
            // Re-generate password hash and password salt
            foundUser.PasswordSalt = HashHelper.CreateRandomSalt();
            foundUser.PasswordHash = HashHelper.ComputeSaltedHash(
                      passwordResetUser.NewPassword, foundUser.PasswordSalt);

            // re-generate passwordAnswer hash and passwordAnswer salt
            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():

/// <summary>
/// Authenticate a user with user name and password
/// </summary>
/// <param name="loginParameters"></param>
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 ProfileResetFlag is set
        // ask the user to reset profile first
        if (e.User is LoginUser)
        {
            if (((LoginUser)e.User).ProfileResetFlag)
            {
                // open the MyProfile screen
                AppMessages.ChangeScreenMessage.Send(ViewTypes.MyProfileView);
                CurrentScreenText = ViewTypes.MyProfileView;
            }
            else
            {
                // otherwise, open the home screen
                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)
{
    // check for insert user permission
    if (CheckUserInsertPermission(user) && user.IsUserMaintenance)
    {
        // validate whether the user already exists
        User foundUser = ObjectContext.Users.Where(
            n => n.Name == user.Name).FirstOrDefault();
        if (foundUser != null)
            throw new ValidationException(ErrorResources.CannotInsertDuplicateUser);

        // Re-generate password hash and password salt
        user.PasswordSalt = HashHelper.CreateRandomSalt();
        user.PasswordHash = HashHelper.ComputeSaltedHash(
                             user.NewPassword, user.PasswordSalt);

        // set a valid PasswordQuestion
        SecurityQuestion securityQuestion = 
          ObjectContext.SecurityQuestions.FirstOrDefault();
        if (securityQuestion != null)
            user.PasswordQuestion = securityQuestion.PasswordQuestion;
        // set PasswordAnswer that no body knows
        user.PasswordAnswerSalt = HashHelper.CreateRandomSalt();
        user.PasswordAnswerHash = HashHelper.CreateRandomSalt();

        // requires the user to reset profile
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Weidong Shen
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
GeneralMy vote of 5 PinmemberJerry_zh1-Mar-12 6:17 
QuestionHi PinmemberGoran _21-Feb-12 13:34 
AnswerRe: Hi PinmemberWeidong Shen21-Feb-12 16:00 
GeneralRe: Hi PinmemberGoran _21-Feb-12 23:42 
GeneralRe: Hi PinmemberWeidong Shen22-Feb-12 3:16 
QuestionNot able to see the login screen. PinmemberLau Kok Yoon17-Dec-11 23:56 
AnswerRe: Not able to see the login screen. PinmemberWeidong Shen18-Dec-11 9:48 
GeneralRe: Not able to see the login screen. PinmemberLau Kok Yoon18-Dec-11 19:12 
QuestionRe: Not able to see the login screen. PinmemberLau Kok Yoon18-Dec-11 19:59 
GeneralRe: Not able to see the login screen. PinmemberLau Kok Yoon18-Dec-11 21:02 
GeneralRe: Not able to see the login screen. PinmemberWeidong Shen19-Dec-11 3:31 
QuestionThe name does not exist in the context. How to get rid of this error? PinmemberLau Kok Yoon14-Dec-11 0:08 
AnswerRe: The name does not exist in the context. How to get rid of this error? PinmemberWeidong Shen14-Dec-11 7:50 
GeneralRe: The name does not exist in the context. How to get rid of this error? PinmemberLau Kok Yoon15-Dec-11 20:26 
GeneralRe: The name does not exist in the context. How to get rid of this error? PinmemberWeidong Shen16-Dec-11 4:31 
GeneralRe: The name does not exist in the context. How to get rid of this error? PinmemberLau Kok Yoon16-Dec-11 22:16 
GeneralRe: The name does not exist in the context. How to get rid of this error? PinmemberLau Kok Yoon16-Dec-11 23:03 
Hi Weidong,
 
I manage to build the the project solution without any error. I have another question, how to make my first silverlight program run?
 
I already have a web project which already set as startup project in the solution. And I follow the structure of your sample, by separate layers into few projects and store in folder client and server. I build the solution and run the web project, but nothing show on the screen.
 
I go through a compare against your sample, and found in your sample web project, there are 3 xap files in the ClientBin as below "IssueVision.Main.xap", "IssueVision.Admin.xap" and "IssueVision.User.xap". Where my first sliverlight web project has got NO any xap file in ClientBin. My question is :- How to make my silverlight application run? or How to generate those xap files?
 
Thank you, and sorry to trouble you.
Wink | ;)
GeneralRe: The name does not exist in the context. How to get rid of this error? PinmemberWeidong Shen17-Dec-11 11:53 
GeneralMy vote of 5 PinmemberLau Kok Yoon13-Dec-11 23:44 
QuestionValidate if record exists before cretaitng it PinmemberTsubaza12-Oct-11 2:50 
AnswerRe: Validate if record exists before cretaitng it PinmemberWeidong Shen12-Oct-11 3:51 
GeneralRe: Validate if record exists before creating it and also Update problem PinmemberTsubaza12-Oct-11 8:03 
GeneralRe: Validate if record exists before creating it and also Update problem PinmemberWeidong Shen13-Oct-11 3:56 
GeneralRe: Validate if record exists before creating it and also Update problem PinmemberTsubaza13-Oct-11 6:06 
GeneralRe: Validate if record exists before creating it and also Update problem PinmemberWeidong Shen13-Oct-11 6:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140415.2 | Last Updated 8 Jul 2011
Article Copyright 2010 by Weidong Shen
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid