ASP.Net MVC Membership Basics

December 10, 2007 4:26 PM

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!

Tags: , ,
Categories: MVC
Actions: E-mail | Permalink | Comments (46) RSS Feed for this post's comments.

Comments

12/10/2007 4:34 PM #

trackback


Trackback from: DotNetKicks.com

12/11/2007 12:09 AM #

scottgu


Very cool - thanks for sharing this!

Scott

scottgu us

12/11/2007 7:30 AM #

Johan

Write <label for="rememberMe">Remember me</label> instead. Then you don´t need the javascript!

Johan se

12/11/2007 9:01 AM #

Steven Harman


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.

Steven Harman us

12/11/2007 9:48 AM #

Troy Goode


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!

Troy Goode us

12/11/2007 10:42 AM #

trackback


Trackback from: SquaredRoot

12/11/2007 1:16 PM #

mikeyb


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.

mikeyb us

12/11/2007 1:30 PM #

Troy Goode


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.

Troy Goode us

12/11/2007 2:24 PM #

Rob Conery


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 Smile.

It'll be fixed in the next drop Smile.

Rob Conery us

12/11/2007 4:54 PM #

Troy Goode

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!

Troy Goode us

12/14/2007 8:18 PM #

pingback


Pingback from: dotnet.robertmao.com

12/17/2007 11:26 AM #

pingback


Pingback from: geekdaily.net

12/17/2007 2:04 PM #

pingback


Pingback from: code-inside.de

12/17/2007 3:22 PM #

pingback


Pingback from: punk.tw

12/18/2007 10:43 AM #

oVan

Thanks, very clear and helpful sample!

oVan be

12/18/2007 10:53 AM #

oVan

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.

oVan be

12/18/2007 11:07 AM #

Nicolas Cadilhac


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

Nicolas Cadilhac ca

12/19/2007 5:57 AM #

oVan


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 be

12/19/2007 2:20 PM #

Troy Goode

oVan, I have fixed the routing.txt file and published a new version of the zip file. Thanks for pointing that out!

Troy Goode us

12/19/2007 2:23 PM #

Troy Goode


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

Troy Goode us

12/19/2007 2:24 PM #

Troy Goode


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

Troy Goode us

12/19/2007 2:28 PM #

Nicolas Cadilhac

oVan, could you explain how to access this ViewContext object ? Where does it come from ? Thanks.

Nicolas Cadilhac ca

12/19/2007 3:10 PM #

oVan

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.

oVan be

12/19/2007 3:18 PM #

Nicolas Cadilhac

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 Cadilhac ca

12/20/2007 9:29 AM #

pingback


12/20/2007 1:46 PM #

Troy Goode


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

Troy Goode us

12/20/2007 2:41 PM #

Nicolas Cadilhac


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.

Nicolas Cadilhac ca

12/24/2007 2:35 AM #

Troy Goode

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!

Troy Goode us

1/2/2008 11:59 PM #

James Nail

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")

James Nail us

1/3/2008 8:42 PM #

Troy Goode

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

Troy Goode us

1/4/2008 9:49 AM #

Troy Goode

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:

www.squaredroot.com/.../...ication-and-Errors.aspx

Troy Goode us

1/12/2008 2:20 PM #

Nicolas Cadilhac

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 Cadilhac ca

1/12/2008 2:59 PM #

Troy Goode

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. Smile

Troy

Troy Goode us

1/12/2008 8:26 PM #

Nicolas Cadilhac

Perfect. Thank you. I had the beginning of your solution but was not aware of this FormsAuthentication.DefaultUrl.

Nicolas Cadilhac ca

1/21/2008 6:40 PM #

Shailen Sukul

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.

Shailen Sukul au

1/21/2008 8:14 PM #

Shailen Sukul

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:
<form name="aspnetForm" method="post" action="Login?ReturnUrl=%2fNews" id="aspnetForm">
...
<form action="/Security/Authenticate" method="post">
....
<input type="submit"/>
</form>
</form>

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.

Shailen Sukul au

1/21/2008 8:19 PM #

Shailen Sukul

Just want to note that I got correct postback behaviour in the MVC View Content page through an ugly hack;
I put a </form> tag before the start of the new <form> Smile Not pretty, but it works!

Shailen Sukul au

1/21/2008 9:11 PM #

Troy Goode

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 <form> tag wrapping around the ContentPlaceholder in your Site.master page.

While it is common practice (and essentially required) to wrap a form element around the entire page while developing with WebForms, this will cause some significant issues in the MVC framework. I recommend removing any <form> tags from the master page (unless you have an actual form in the master page, like a search box) and only placing the <form> tag around the actual form areas of your views.

Hope that makes sense,

Troy

Troy Goode us

1/21/2008 9:24 PM #

Shailen Sukul

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.

Shailen Sukul au

1/22/2008 11:10 AM #

Troy Goode

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

Troy Goode us

2/12/2008 6:12 AM #

pingback


Pingback from: benmarsh.co.uk

2/22/2008 4:03 PM #

Rafael Pol

Very helpful, Thanks.

Rafael Pol do

4/2/2008 8:32 PM #

Troy Goode

I have updated the code to work with Preview 2 of the MVC framework. For more information please see:

www.squaredroot.com/.../...ership-Starter-Kit.aspx

or

http://www.codeplex.com/MvcMembership

I am closing comments on this thread since it is outdated.

Troy Goode us

5/4/2008 4:32 PM #

pingback


Pingback from: johanbenjaminsson.se

6/25/2008 9:49 PM #

pingback


Pingback from: jason.whitehorn.ws

6/27/2008 1:17 PM #

pingback


Comments are closed

Troy Goode

Troy Goode
Microsoft Certified Professional Developer
AddThis Feed Button

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in  anyway.

© Copyright 2008

Colophon

Powered by:
BlogEngine.NET 1.4
Template:
Designs by Darren
Header Font:
Stamper
Syntax Highlighting:
WLW Code Snippet Plugin