Custom Membership Providers - Task Manager
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 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 <b>user.GetUserIDByUserName(userName)</b> 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);
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."); } } // If we got this far, something failed, redisplay form 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)); } } // If we got this far, something failed, redisplay form 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 various pages. Lets call this as "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 "DevUser"; } } 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, an instance of 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 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 now 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. It's given below:
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 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 just add a tab called "Task Manager", to the master page, so that we have a link through we can access the task manager. Open the 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 by 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 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
发表评论
1jh6eo A big thank you for your post.Thanks Again. Cool.
vIQmZi Wow, great blog.Really looking forward to read more. Great.
Awesome blog! Is your theme custom made or did you download it from somewhere? A theme like yours with a few simple tweeks would really make my blog stand out. Please let me know where you got your design. Thank you
I have read a few good stuff hereDefinitely price bookmarking for revisitingI wonder how much attempt you set to create this kind of great informative website.
I just came across this blog I was I am getting a 404 page not found when trying to access other posts I hope it gets fixed. Thanks
Hello! I just discovered your webpage: %BLOGTITLE% when I was exploring digg.com. It looks as though someone liked your website so much they decided to bookmark it. I'll absolutely be coming here more often.