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)
{
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.
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.
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.