Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom Membership Providers - Task Manager

0.00/5 (No votes)
9 Apr 2011 3  
This article extends what was built in the previous article. This is a task manager application that uses the custom provider designed in the last part.

Introduction

In the last article, we saw how a custom membership provider could be implemented so as to make effective use of the Forms Authentication model. Now let us extend this application to a task manager application that uses the authentication data to distinguish the users and perform activities related to that particular user. Remember that you have to modify the "AppDb" connection string in the sample project to point to your database.

Part 1 of the article is available here.

Extending the Application

At this point, we have an elementary app. That authenticates using our custom membership provider class. Now, let us extend this to an app that could be used by users to enter tasks they want to remember. Its per user and so we need to have a way by which we can identify the current user. This information would be required while adding tasks and listing tasks. To do this, firstly we need to create a forms authentication ticket as shown below. This would be in the "Register" action in the "Account" controller. Now add a method to the AccountController.cs file, as shown below:

private void SetupFormsAuthTicket(string userName, bool persistanceFlag)
{
        User user = new Models.User();
        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,
                                userName,
                                DateTime.Now,
                                DateTime.Now.AddMinutes(30),
                                persistanceFlag,
                                user.GetUserIDByUsername(userName).ToString());

        string encTicket = FormsAuthentication.Encrypt(authTicket);
        this.Response.Cookies.Add(
             new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
}

Notice the line user.GetUserIDByUserName(userName) in the list of parameters passed to the constructor of the FormsAuthenticationTicket class. This is the "userData" property which could be used to store our custom data that could be used elsewhere. For your reference at this point, I have added the user ID of the current user in the Forms Authentication ticket. I have given below the signature of the FormsAuthenticationTicket class' constructor:

public FormsAuthenticationTicket(int version, string name, 
       DateTime issueDate, DateTime expiration, 
       bool isPersistent, string userData);

The next step would be to modify the LogOn and Register methods to call this method, if the user was successfully logged on or registered. I have given below the modified versions of the LogOn and Register methods.

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);

            SetupFormsAuthTicket(model.UserName,model.RememberMe);

            if (!String.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", 
              "The user name or password provided is incorrect.");
        }
    }

    return View(model);
}

[HttpPost]
public ActionResult Register(RegisterModel model)
{
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        MembershipCreateStatus createStatus = MembershipService.CreateUser(
                 model.UserName, model.Password, model.Email);

        if (createStatus == MembershipCreateStatus.Success)
        {
            FormsService.SignIn(model.UserName, false);

            SetupFormsAuthTicket(model.UserName,false);
                
            return RedirectToAction("Index", "Home");
        }
        else
        {
            ModelState.AddModelError("", 
               AccountValidation.ErrorCodeToString(createStatus));
        }
    }

    ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
    return View(model);
}

What Next?

Now we have the Forms Authentication ticket with the user ID of the current user. We have got to add a way for the views to consume this. For this purpose, let us create a class that holds this ticket and the information that would be required by the various pages. Let's call this "UserIdentity". It has to implement the IIdentity interface. The purpose of implementing this interface would be clear in the following steps. Notice the "UserId" property which returns the "UserData" of the ticket that contains the user ID of the currently logged on user.

public class UserIdentity : IIdentity
{
    private System.Web.Security.FormsAuthenticationTicket ticket;

    public UserIdentity(System.Web.Security.FormsAuthenticationTicket _ticket)
    {
        ticket = _ticket;
    }

    public string AuthenticationType
    {
        get { return "User"; }
    }

    public bool IsAuthenticated
    {
        get { return true; }
    }

    public string Name
    {
        get { return ticket.Name; }
    }

    public string UserId
    {
        get { return ticket.UserData; }
    }
}

Wrapping it Up

Now in order to let all the pages use this information, we need to set this information to the HttpContext.Current.User object. This is done in the Global.ascx file as shown below. Add an OnInit method to the file that attaches a PostAuthenticateRequest event.

In the method called during the PostAuthenticateRequest event, notice that the authentication ticket is decrypted, and an instance of the UserIdentity object is created and passed on to GenericPrincipal's constructor to create a GenericPrincipal object. This is the reason why UserIdentity had to implement the IIdentity interface. Finally, the principal object is assigned to the HttpContext.Current.User object.

public override void Init()
{
    this.PostAuthenticateRequest += 
         new EventHandler(MvcApplication_PostAuthenticateRequest);
    base.Init();
}

void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
    HttpCookie authCookie = 
      HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        string encTicket = authCookie.Value;
        if (!String.IsNullOrEmpty(encTicket))
        {
            FormsAuthenticationTicket ticket = 
                   FormsAuthentication.Decrypt(encTicket);
            UserIdentity id = new UserIdentity(ticket);
            GenericPrincipal prin = new GenericPrincipal(id, null);
            HttpContext.Current.User = prin;
        }
    }
}

Creating the Table(s) Required

Now we have the required information in the User object. Before seeing how we can use it, let us create a table for holding the tasks that users can add/view. The structure of the table is quite simple. Note that it has a foreign key constraint with the Users table created earlier for holding our users.

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[Tasks](
    [TaskID] [int] IDENTITY(1,1) NOT NULL,
    [TaskName] [varchar](50) NOT NULL,
    [TaskDescription] [varchar](255) NOT NULL,
    [TaskDateTime] [datetime] NOT NULL,
    [UserID] [int] NOT NULL,
    CONSTRAINT [PK_Tasks] PRIMARY KEY CLUSTERED 
(
    [TaskID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
       IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
       ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO

ALTER TABLE [dbo].[Tasks]  WITH CHECK 
      ADD  CONSTRAINT [FK_Tasks_Users] FOREIGN KEY([UserID])
REFERENCES [dbo].[Users] ([UserID])
GO

ALTER TABLE [dbo].[Tasks] CHECK CONSTRAINT [FK_Tasks_Users]
GO

The "Tasks" Repository

Now that we have a table to represent a user's tasks, let us create an object to hold the entries in this table. Given below is the TaskObj:

[Table(Name = "Tasks")]
public class TaskObj
{
    [Column(IsPrimaryKey = true, 
      IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
    public int TaskID { get; set; }
    [Column] public string TaskName { get; set; }
    [Column] public string TaskDescription { get; set; }
    [Column] public DateTime TaskDateTime { get; set; }
    [Column] public int UserID { get; set; }
}

The next step would be to create the repository that would add a task or get a list of tasks by user ID. The project attached has additional methods to get a certain task and to delete a task, which would be used in the future enhancements for this article. Given below is the task repository:

public class Task
{
    private Table<TaskObj> taskList;
    private DataContext context;

    public Task()
    {
        string connectionString = 
          ConfigurationManager.ConnectionStrings["AppDb"].ConnectionString;
        context = new DataContext(connectionString);
        taskList = context.GetTable<TaskObj>();
    }

    public IEnumerable<TaskObj> GetTasksByUserID(int userID)
    {
        return taskList.Where(t => t.UserID == 
                  userID).OrderByDescending(t => t.TaskDateTime);
    }

    public bool AddTask(TaskObj task)
    {
        taskList.InsertOnSubmit(task);
        context.SubmitChanges();
        return true;
    }
}

Using the User ID Available in the Views

We come to the most interesting part (at least for this article!). How do we use the user related information we stored with the forms auth ticket? You are not far from seeing that! You just have to grab the User.Identity object from the System.Security.Principal.IPrincipal.Controller object. It's of type IIdentity that we saw earlier while constructing the UserIdentity type. So, we could safely cast it to UserIdentity.

If you recollect the UserIdentity type, we added a property called UserId which contains the user data we stored in the Forms Authentication ticket. This could be used in the controller for adding a task or listing the tasks associated to this user ID. Check out the controller for adding a task below - see how the user ID is obtained. Also, notice the Authorize attribute for the controller. Once we get the user ID, I just call the tasks repository to add the task.

[Authorize]
[HttpPost]
public ActionResult AddTask(TaskObj task)
{
    UserIdentity identity = (UserIdentity)User.Identity;
    Task _task = new Task();
    task.UserID = int.Parse(identity.UserId);
    task.TaskDateTime = DateTime.Now;
    _task.AddTask(task);

    return View(task);
}

Now, let us add a tab called "Task Manager" to the master page so that we have a link through which we can access the task manager. Open Site.Master under the Views/Shared folder and add the following line:

<li>
    <%: Html.ActionLink("Task Manager", "Tasks", "Home") %>
</li>

Now add the views Tasks.aspx and AddTask.aspx under the Home folder. The Tasks view's model is strongly typed to an IEnumerable of TaskObj and simply lists the tasks using foreach. The controller for Tasks gets the list of tasks for the user using the GetTasksByUserID method in the Tasks repository. It gets the user's ID from the UserIdentity object as shown before in the AddTask controller.

1.jpg

AddTask, on the other hand, is strongly typed to the TaskObj type and we use Html.BeginForm to display the user with a form, and in the controller, we call the AddTask method in the Tasks repository to add the task. You could have a look at the views in the project attached.

2.jpg

Future Improvements Planned

In future, I plan to update this article to use Dependency Injection and I also plan to add AJAX support to add/edit/delete tasks with a common management page instead of having separate pages for add/edit/delete.

Conclusion

In my first article, I explained how custom membership providers can be used for controlling how the users are authenticated in our application and in this article, I have extended that application to do something more useful than just authenticate a user. I hope this article was helpful to you all! Please feel free to post comments/questions in the comments section below. Again, thanks for reading my article!

History

First version released.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here