ASP.Net MVC Membership Basics
// December 10th, 2007 // MVC
NOTE:
This article was written for the December CTP release of the MVC framework. Unfortunately, it does not entirely apply to the Preview 2 release or subsequent releases.
The MVC bits have finally arrived and I’ve spent a while digesting them. I’ve been waiting for the bits to be released to begin working on a side-project, so the first thing I did after downloading them last night was crank it up and start working on a new ASP.NET MVC Web Application project.
Typically, the first thing I do on a new project is set up the authentication/authorization system. For the project I am working on I want to use the ASP.Net Membership system, but most of the Membership controls do not work with the MVC framework because they require postbacks. I spent some time last night building a Security controller and views for Registration and Login that I thought would be worth sharing.
So far I have implemented basic functionality for Register, Login, and Logout. There are three files we will have to create. We will also have to change the routing table. Let’s start with the SecurityController:
1 public class SecurityController : Controller
2 {
3
4 [ControllerAction]
5 public void Login()
6 {
7 RenderView("Login");
8 }
9
10 [ControllerAction]
11 public void Register()
12 {
13 RenderView( "Register" );
14 }
15
16 [ControllerAction]
17 public void Logout()
18 {
19 FormsAuthentication.SignOut();
20 Response.Redirect( "/" );
21 }
22
23 [ControllerAction]
24 public void Authenticate( string userName, string password, string rememberMe, string returnUrl )
25 {
26 // figure out if username and password are correct
27 if( Membership.ValidateUser( userName, password ) )
28 {
29 // everything is good, create an authticket and go
30 FormsAuthentication.SetAuthCookie( userName, (rememberMe != null) );
31 Response.Redirect( returnUrl );
32 }
33 else
34 {
35 // something was wrong, figure out which and pass it into view
36 if( Membership.GetUser(userName) == null )
37 ViewData["ErrorMessage"] = "Incorrect username.";
38 else
39 ViewData["ErrorMessage"] = "Incorrect password.";
40 RenderView( "Login" );
41 }
42 }
43
44 [ControllerAction]
45 public void CreateUser( string userName, string emailAddress, string password, string returnUrl )
46 {
47 try
48 {
49 // try to create user and then login that user
50 if( Membership.CreateUser( userName, password, emailAddress ) == null )
51 throw new MembershipCreateUserException( "An unspecified error occurred." );
52 FormsAuthentication.SetAuthCookie( userName, true );
53 Response.Redirect( returnUrl );
54 }
55 catch( MembershipCreateUserException e )
56 {
57 // something went wrong
58 ViewData["ErrorMessage"] = e.Message;
59 RenderView("Register");
60 return;
61 }
62 }
63
64 }
So we have two basic actions methods that only display views (Register and Login) and three action-only methods that have no views (CreateUser, Authenticate, Logout).
I have chosen to not have a logout page, but to instead redirect to the homepage. Switch out ‘Response.Redirect( "/" );’ for ‘RenderView( "Logout" );’ and create a Logout.aspx view if you would like to display a logout message.
Now let’s look at the Login view:
1 <asp:Content ID="content" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
2
3 <h2>Login</h2>
4
5 <% if( ViewData["ErrorMessage"] != null ){ %>
6 <p><% =ViewData["ErrorMessage"] %></p>
7 <% } %>
8
9 <% using(Html.Form( "Authenticate", "Security" )){ %>
10 <fieldset>
11 <legend>Login</legend>
12 <div><label for="userName">User Name:</label> <% =Html.TextBox( "userName" ) %></div>
13 <div><label for="password">Password:</label> <% =Html.Password( "password" ) %></div>
14 <div><label for="rememberMe">Remember Me:</label>
15 <input type="checkbox" id="rememberMe" name="rememberMe" checked="checked" value="checked" /></div>
16 <div><% =Html.SubmitButton() %></div>
17 <% =Html.Hidden( "returnUrl", "/" ) %>
18 </fieldset>
19 <% } %>
20
21 </asp:Content>
The toolkit’s Html.Checkbox(…) method annoys me. More on why in another post. For now I’ve instead just written the html out by hand. You’ll note I’ve also linked the label to the checkbox with JavaScript so that clicking the label toggles the checkbox.
Then the Register view:
1 <asp:Content ID="content" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
2
3 <h2>Register</h2>
4
5 <% if( ViewData["ErrorMessage"] != null ){ %>
6 <p><% =ViewData["ErrorMessage"] %></p>
7 <% } %>
8
9 <% using(Html.Form( "CreateUser", "Security" )){ %>
10 <fieldset>
11 <legend>Register</legend>
12 <div><label for="userName">User Name:</label> <% =Html.TextBox( "userName" ) %></div>
13 <div><label for="emailAddress">Email Address:</label> <% =Html.TextBox( "emailAddress" ) %></div>
14 <div><label for="password">Password:</label> <% =Html.Password( "password" ) %></div>
15 <div><% =Html.SubmitButton() %></div>
16 <% =Html.Hidden( "returnUrl", "/" ) %>
17 </fieldset>
18 <% } %>
19
20 </asp:Content>
Another straightforward view. Not much to discuss here.
And finally, let’s add the new routes:
1 RouteTable.Routes.Add( new Route
2 {
3 Url = "Login",
4 Defaults = new {
5 controller = "Security",
6 action = "Login" },
7 RouteHandler = typeof( MvcRouteHandler )
8 } );
9 RouteTable.Routes.Add( new Route
10 {
11 Url = "Register",
12 Defaults = new {
13 controller = "Security",
14 action = "Register" },
15 RouteHandler = typeof( MvcRouteHandler )
16 } );
I personally like login to be http://website/login and register to be http://website/register, so that is how I have configured it. The other three actions (Logout, Authenticate, and CreateUser) I access via the default route (ex: /Security/Logout).
That’s it! You should now have a working registration/login system. I’ll leave making it pretty with CSS as an exercise for the reader.
I have included all of the code samples above in the below ZIP file. Just unzip it and place the controller into the Controllers directory, the views into the Views/Security directory (which you will have to create), and copy the code from Routes.txt to the appropriate area of your Global.asax.
MVCMembership_v1.2.zip (2.03 kb)
UPDATE (Dec 11): Johan and Steve Harman were kind enough to point out that I had foolishly set the "remember me" checkbox’s label’s "for" attribute to point to the password field instead of the checkbox itself. I have fixed the code above and provided a new zip file (1.1) for download. Thanks guys!
Update (Dec 19): oVan pointed out a bug in the routing rules defined in the routes.txt file. I have updated the zip file with the correct code. Thanks oVan!
Update (Jan 3): James Nail asked a very good question via a comment: what do you set for the loginUrl and defaultUrl in your web.config? Well James, here is how I’ve setup my web.config…
Assuming we’ll be using forms authentication and securing all pages except login and the homepage, place the following inside the <system.web>element:
1 <authentication mode="Forms">
2 <forms loginUrl="/Login" defaultUrl="/" />
3 </authentication>
4 <authorization>
5 <deny users="?" />
6 </authorization>
Then, somewhere outside the <system.web> element add:
1 <location path="Default.aspx">
2 <system.web>
3 <authorization>
4 <allow users="*" />
5 </authorization>
6 </system.web>
7 </location>
8 <location path="Security">
9 <system.web>
10 <authorization>
11 <allow users="*" />
12 </authorization>
13 </system.web>
14 </location>
15 <location path="Login">
16 <system.web>
17 <authorization>
18 <allow users="*" />
19 </authorization>
20 </system.web>
21 </location>
Note that this will grant access to all actions within the Security controller. It is also worth pointing out that, dependent on your setup, your CSS and image files may not load unless you also create a location path for their directory and grant all users access like so:
1 <location path="Content">
2 <system.web>
3 <authorization>
4 <allow users="*" />
5 </authorization>
6 </system.web>
7 </location>
I have not updated the zip file with these web.config settings; let me know if anyone would prefer that I add it. I hope this helps some of you!




Very cool – thanks for sharing this!
Scott
Write instead. Then you don´t need the javascript!
Just wanted add to what Johan said, you don’t need the Javascript on the label for the checkbox as long as you specify the label’s “for” attribute has a value equal another element’s “id” attribute the browser will do the right thing.
Meaning, if you specify the for=”id-of-a-textbox” it will focus on the associated textbox when you click on the label. If the for=”id-of-a-checbox” or for=”id-of-a-radiobutton” the browser will toggle the checked=”checked” attribute of the element.
Oops, you’ll notice that I actually specified the label’s “for” attribute properly on all of the textboxes and then accidently left the “for” attribute of the “Remember Me” label set to “password”, which is why it wasn’t working (thus me using the javascript). Thats what I get for being too excited and diving into the new framework late at night. =)
I’ll update the post and zip file shortly. Thanks Johan and Steve!
Very helpful, thanks.
Just one comment. I usually don’t acknowledge which field (username/password) is incorrect. It basically gives someone 1/2 of the equation. Not really that big of a deal, but if you have a user name “admin” why would you want the goofball trying to hack your system to know that.
mikeby,
You’re absolutely right; I believe best practices dictate that you should not distinguish between an incorrect username and an incorrect password. I struggled with whether or not to include it in the sample, but in the end I decided it would be easier for someone to strip that portion of the code out than to figure out exactly which membership method to use if they wanted to distinguish between the two.
Strangely enough, it was two days ago when I was creating a login form, specifically with a “Remember Me” checkbox, that I realized I goofed up the Checkbox() method
.
It’ll be fixed in the next drop
.
Thats great news Rob, thanks. I’ll be looking forward to the next drop, but I think this drop has plenty of stuff to play with for now anyway!
Thanks, very clear and helpful sample!
Small error in the download file (v1.1): the routes.txt file contains an wrong “Register” action for the “Login” url, something that is correct in your code above.
Question from a developer completely new at asp.net and MVC:
How would you protect MVC actions against non authorized access (globally or per action)? Can it be done from the routing table? Does it have to be coded inside each action?
Thanks a lot
A suggestion for improvement:
When you use RenderView() along with ViewData to display an error message in the original view (e.g. Login, Register), you end up seeing the url of the Authenticate and CreateUser action in the address bar (e.g. http://mydomain/Security/Authenticate). At least that’s what happens here with my routing table, where the /Security/Authenticate is hard-coded. Maybe you won’t see it if you use a generic routing rule to arrive there.
Anyway, you can replace the RenderView(”Login”) with RedirectToAction(”Login”), that will give you back the original url.
Don’t forget to change ViewData["ErrorMessage"] to ViewContext.TempData["ErrorMessage"] everywhere, or you’ll see an empty ViewData collection
oVan, I have fixed the routing.txt file and published a new version of the zip file. Thanks for pointing that out!
Nicolas,
You have several options for protecting against unauthorized access, but they all primarily involve modifying either every action or creating a base controller that does the authorization before an action is called. Unfortunately there is no way to define authorization rules within the routing table.
I’ll be creating a post soon (hopefully tonight) that contains code samples for making this much easier.
Troy
oVan,
That is a good tip. I am planning on updating the membership code sample and will try to incorporate the solution you have mentioned. Thanks for another great tip!
Troy
oVan, could you explain how to access this ViewContext object ? Where does it come from ? Thanks.
Nicolas, I didn’t find much info on it yet, seems documentation is lacking. It’s defined in System.Web.Mvc and I just found out by debugging to see where my TempData was defined.
Thank you oVan, but I mean, I don’t even know how to access this context or TempData in the view to display the error message. Real beginner here… Thx
Nicolas & oVan,
I have written a new article on the TempData collection that you both may want to check out. I hope it helps!
Troy
Thanks a lot Troy. And anyway, I got it working. Yesterday TempData was not found by VS and today it compiled. Don’t ask me why.
Thanks again for this nice article.
I know I promised some code samples for authorization stuff, but unfortunately the holidays have been keeping me a bit too busy. I did a bunch of work on the plan to and from Oklahoma though and should have some interesting stuff to show everyone in a day or two!
Hi Troy,
Thanks for a very helpful post… one thing I can’t seem to get though, is the web.config sections for the membership provider… specifically, what do you set for the loginUrl & defaultUrl attributes of the forms element (forms authentication section)? (ex., loginUrl=”Login.aspx” defaultUrl=”Default.aspx”)
Hi James,
Great question! I have update the post with the details of how to configure your web.config file. I hope it helps!
Troy
Hey guys, I mentioned that I was working on something to make the authentication model friendlier and it’s done! Take a look at my latest post:
http://www.squaredroot.com/post/2008/01/MVC-Authentication-and-Errors.aspx
When a user just logged in, is it possible to redirect him to the page he had requested? Right now the call to Response.Redirect( returnUrl ); brings me to “/” (which is set in the web.config file).
Nicolas,
By default ASP.Net will include a querystring parameter of “?ReturnUrl=X” where X is the requested url when it redirects a user to the login page. So if you are using the login view I’ve provided, simply changing this line (line 17):
< % =Html.Hidden( "returnUrl", "/" ) %>
to:
< % =Html.Hidden( "returnUrl", Request.QueryString["ReturnUrl"] ?? FormsAuthentication.DefaultUrl ) %>
… should do the trick. In fact that is the way I should’ve done it in the first place! Let me know if you have any other problems.
Troy
Perfect. Thank you. I had the beginning of your solution but was not aware of this FormsAuthentication.DefaultUrl.
This may seem a bit silly, but it should be noted that the 2 RouteTable.Routes.Add statements need to be the FIRST 2 statements on the Global.asax file because the MVC routing will handle the first route match in a top down fashion.
If the statements are placed after the default routing statement then you will not be able to redirect to the login page.
I have nearly all the features working from this tutorial, except for one.
I created the Login.aspx form as an MVC View Content page. When it renders, the HTML shows a form within a form, ie something like this:
Guess what happens when it is posted. The Index method is called instead of the Authenticate method because the top-most form’s url posts. This problem is not related to MVC, rather it is a HTML problem.
I tried again, using an MVC View Page and it works. It is annoying though, coz the Login View Page looks ugly without the Master styles.
Thanks.
Just want to note that I got correct postback behaviour in the MVC View Content page through an ugly hack;
I put a tag before the start of the new
Hi Shailen,
In the example you posted above the behavior you were experiencing is due to the nested forms as you mentioned. This is most likely occurring because you have a
I played around with the form tags in the master page, but it blows up my site because I have my menu embedded in there.
Seems like I will have to stick with that hack for now. It will be interesting to note how MS will fix this issue…
Thanks for your input.
Hi Shailen,
I’d be more than happy to try and help you discover the root of your problem if you’d like. Email me a copy of the master page and the login view if you’d like me to take a look:
tgoode@squaredroot.com
Troy
Very helpful, Thanks.
I have updated the code to work with Preview 2 of the MVC framework. For more information please see:
http://www.squaredroot.com/post/2008/04/MVC-Membership-Starter-Kit.aspx
or
http://www.codeplex.com/MvcMembership
I am closing comments on this thread since it is outdated.
[...] MVC – Basic CategoriasAuthentication, MVC, Security, asp.net Comentários (0) Trackbacks (0) Deixe um comentário Trackback [...]