Ayende @ Rahien

It's a girl

Castle Demo App: ViewComponents, Security, Filters and Friends

In this post, I'm going to explain how to secure the administration section of the site. Since the pages in our application do not map to a directory structure on the server, the ASP.Net default security isn't going to help us (I'm not entirely sure about this, though).

Because of the way that the MonoRail is built, we often can make security decisions about the all the actions in a controller. Such is the case with the AdminController, only administrators should be able to access this page. Currently, we don't have the notion of an admin in the application, so we will need to add that as well.

We want to think about this for a minute, do we want a seperate page for login, or do we want to have a login box in every page? I think that we want a login box in every page, which can either display the Enter User and Password, or the currently logged on user. We can add it to the layouts, and then it will be seen in all the pages that belongs to this layout. But, we have multiply layouts in the application. Right now we have two of those, but we may have more in the future. So we can't just put it in the layout.

If I was using ASP.Net, I could've created a control (or used a pre-built one). In MonoRail, this is called a ViewComponent, and we'll build one in a few minutes. Just to note, in Brail, we can often just use a Common Script method, but because ViewComponents has access to MonoRail state, it's often simpler to just use them when you need to access various parts of MonoRail. In this case, we need to determain if the user it logged on or not, so we'll build a ViewComponent for this. Here is the code for the LoginComponent:

public class Login : ViewComponent

{

    public override void Initialize()

    {

        Flash["user"] = this.RailsContext.CurrentUser.Identity;

    }

    public override void Render()

    {

        if (this.RailsContext.CurrentUser.Identity.IsAuthenticated==false)

            RenderView("Login");

        else

            RenderView("LoggedOn");

    }

}

We put the current user identity in the Flash on intialization (so the view can make use of it, if applicable), and when we render the control, we choose which view to render, the Login control, or the LoggedOn notification. If you are developing using the WebDev.WebServer, you already has Windows Authentication on, we want to disable that for this app (I want to show how we can build our own). You can disable Windows Authentication by updating the Web.Config file to include this little snippet.

<system.web>

     ...

<authentication mode="Forms"/>

...

</system.web>

A couple of things about ViewComponents, they are like tiny controllers, but they should be focused on doing spesific stuff, usually providing some sort of a all around service, like this login control. This is a way where you can still get the seperation of concerns that I like so much in MonoRail, and get functionality that can cut through all the layers of the application without breaking the design.

Now for the details, the views for a component are located on Views\Components\<Component Name>, and the default view for a component is Default.boo (no surprise here, sorry). So the Login view is located at Views\Componenets\Login\Login.boo, and looks like this:

${HtmlHelper.Form('/Home/AuthenticateUser.rails')}

${HtmlHelper.FieldSet('Login:')}  

<table>

      <tr>

            <td>${HtmlHelper.LabelFor('userName','Username:')}</td>

            <td>${HtmlHelper.InputText('userName','')}</td>

      </tr>

      <tr>

            <td>${HtmlHelper.LabelFor('password','Password:')}</td>

            <td>${HtmlHelper.InputPassword('password')}</td>

      </tr>

</table>  

 ${HtmlHelper.InputHidden('currentAction', currentAction)}

<p>

      ${HtmlHelper.SubmitButton('Login')}

</p>

${HtmlHelper.EndFieldSet()}

${HtmlHelper.EndForm()}

Nothing much going here, we have a user and password fields, and we direct them to the AuthenticateUser action. The LoggedOn view is located at Views\Components\Login\LoggedOn.boo and is defined so:

${HtmlHelper.FieldSet('User Information:')}

<table>

      <tr >

            <td>${HtmlHelper.LabelFor('userNameLabel','Username:')}</td>

            <td>${HtmlHelper.LabelFor('userName',user.Name)}</td>

      </tr>

      <tr>

          <td>${HtmlHelper.LinkTo('Logout','home','logout')}</td>

      </tr>

</table>

${HtmlHelper.EndFieldSet()}

Again, this is nothing new, just showing the user name and a logout link. Now, let's add that to the Home layout (located at Views\Layouts\Home.boo), and edit it so it will look like this:

<!DOCTYPE xhtml

PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>

<body>

<table>

      <tr>

            <td>

                  ${ChildOutput}

            </td>

            <td width="20%">

                  <?brail component Login ?>

            </td>

      </tr>

</body>

</html >

(I hear the CSS Designers' League is on the lookout after me because of this series :-D). As you can see, we only need to specify which comonent we want, and that is it. We also need to make the same change to the Admin layout. Compile the application and point your browser to: http://localhost:8080/home/default.rails, here is how it looks like on my system:

Okay, we now have a login control, let's put it to good use. We'll add the Authenticate User action to the HomeController, but before we can do that we need to consider some design implications of this change. We will probably need to give the user some error message if they couldn't login, but we already wrote that when we did the AdminController. I get the shakes when I see duplicate code, so we need to do a couple of things here, we need to make the GenericMessage view aviable to all the controllers in the application, and we probably want to make the RenderMessage() method accessible to the HomeController as well.

Well, a view is really something that is relevant for a single controller, but luckily for us, we don't need to duplicate the view, since MonoRail has the notion of a Shared View. We need to move GenericMessage.boo from Views\Admin\ to Views\ (to make it visible for other controllers).

Now, we need to make RenderMessage() accessible for the HomeController. I choose to do this with Extract Base Class, and create the following class, which both AdminController and HomeController now inherits from:

public abstract class MythicalBugTrackerControllerBase : ARSmartDispatcherController

{

    protected void RenderMessage(string nextActionText, string nextAction)

    {

        PropertyBag["nextActionText"] = nextActionText;

        PropertyBag["nextAction"] = nextAction;

        RenderSharedView("GenericMessage");

    }

}

This is an abstract class that inherits from ARSmartDispatcherController (which in turn inherits from SmartDispatcherController, so we get the nice features of being able to get real method parameters instead of unpacking the request parameters manually). Note that we are using RenderSharedView() to render GenericMessage. Another thing to note is that I belong to the school of thought that is not afraid of long class names.

It's important to note that I'm wrapping the render view call with a method that make sure that the mandatory parameters are set. This ensure that I won't forget to add a parameter and cause my view to throw an exception. I find that this works quite well to create an interface between my views and te controller.

Now, we need to write the code that will handle the security infrastructure for the application. A couple of notes on that before we continue. This is what I call Reasonably Secured application, which means that it's not easy to by-pass the security, but it's not impossible. The security scheme I'm going to show you is vulnerable to attacks such as Man In The Middle and Cookies Replay. If you want true security, you need an expert. If you work in Israel, I can refer you to a superb expert in this field.
Our bug tracking application isn't important enough to make it truely secured (which take more than a bit of work), but we can make it reasonably secured, and I think that this would be enough in this case.

We'll try to take advantage of as many features that ASP.Net provides us as possible, so we'll start with making our User class implement IPrincipal, which just mean that we need these two methods:

public class User : ActiveRecordValidationBase<User>, IPrincipal

{

  ....

 

#region
IPrincipal Members

 

    public IIdentity Identity

    {

        get

        {

            return new GenericIdentity(Name, "Mythical.Bug.Tracker.Authentication");

        }

    }

 

    //TODO: Add support for this later

    public bool IsInRole(string role)

    {

        return false;

    }

 

    #endregion

  ....

}

Right now we don't offer roles, so we always return false (we might add roles later), and the Identity is just the generic identity. Now that we have an IPrinicpal, we can start implementing the login features, here is the Authenticate User method:

    public void AuthenticateUser(string userName, string password, string currentAction)

    {

        User user = User.FindUserByLogin(userName, password);

        if (user == null)

        {

            Flash["bad"] = "User / Password mismatch";

            RenderMessage("Back to home page", "default");

            return;

        }

        AddAuthenticationTicket(user);

 

        Redirect(currentAction);

    }

    private void AddAuthenticationTicket(User user)

    {

        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, user.Name,

            DateTime.Now, DateTime.Now.AddMinutes(20), false, user.Id.ToString());

        string cookieValue = FormsAuthentication.Encrypt(ticket);

        this.Context.Response.CreateCookie(FormsAuthentication.FormsCookieName, cookieValue,

            DateTime.Now.AddMinutes(20));

        this.Context.CurrentUser = user;

    }

We are doing a couple of cool things here. We start by trying to find a user by its name and password, if we can't find it, we send the user to the error page. If we do find a user, we create an authentication ticket for this user (using the pre-existing Froms Authentication in ASP.Net), encrypt it, and store it in a cookie. A couple of points before moving on:

  • Because we are using the existing support in ASP.Net, we saved a whole lot of coding that we may have had to do.
  • We also do here is replace the current user, this is needed so anything that queries the current user in this request (such as the Login component) will recognize the user that just logged in.
  • Notice that the cookie is encrypted and hashed, which means that it's not likely that someone could tamper with our cookie. This is important, since it means that when we get the cookie back, we can rely on its contents (but only if they pass decryption and checks, of course).
  • Since the Authenticate view doesn't have any UI to display, we call the Default() method (which push some parameters for the view to handle) and then specify that we want to use the Default view. It's very easy to do this kind of switching, since we are dealing with the same object, and not another page, which requires some coordination between classes. The only thing that I needed to to support this was to add a call to RenderView("Default") in the Default() method, so the correct view will be rendered, even if we are calling it from another action.

Now that we have an encrypted cookie on the client, let's see how we handle the authentication on the next request. We do this by hooking the AuthenticateRequest event in MythicalBugTrackerApplication. The code that does the check is here:

    void MythicalBugTrackerApplication_AuthenticateRequest(object sender, EventArgs e)

    {

        HttpCookie cookie = Request.Cookies.Get(FormsAuthentication.FormsCookieName);

        if (cookie == null)

            return;

        int id = GetUserId(cookie);

        Model.User current = Model.User.TryFind(id);

        if (current == null)

        {

            //This means that we've a cookie for a user that has been removed, we'll

            //remove the cookie and redirect to the default page, if the user will

            //try to log in again, they will get the usual message, and then it is

            //the IT problem.

            RemoveAuthCookieAndRedirectToDefaultPage();

        }

        Context.User = current;

    }

We get the cookie from the incoming request, and send it to the GetUserId() method (which we'll see in a few seconds). Then we take the user id and try to get the matching user. If the user exists, then we set the Context.User property to this user (which we can do because our User implements IPrincipal). It's possible that the user does not exist (it may have been removed, for instance), so we handle that be removing the authentication cookie and redirecting to the default page. We now have authentication working for our app. Now, let's see some of the details:

    private int GetUserId(HttpCookie cookie)

    {

        try

        {

            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);

            return int.Parse(ticket.UserData);

        }

        catch (ArgumentException ae)

        {

            logger.ErrorFormat(
                "A possible hack attempt, got an invalid cookie value: '{0}' from {1}",

                cookie.Value, Request.UserHostAddress);

            //Somebody tried to mess with the cookie, could be a hacker or transimission

            //failure, we'll remove the cookie and send them to the default page.

            RemoveAuthCookieAndRedirectToDefaultPage();

            return -1;//will never reach here, the previous method will abort the thread

        }

    }

Most of this method is error handling, but basically what happens here is that the cookie in decrypted, and then the id we put there is extracted and returned. The interesting part is the error handling. It's possible that the Decrypt() method (or the Parse(), for that matter) will throw, and in this case, it is most likely a hacker tampering with the cookie. We log the error and then remove the cookie and redirect to the default page in this case. The thinking here is that it may be a transmission error, and anyway, we don't want to provide any information to the hackers (even something like; "modifed cookie values found, kicking you out" is giving away too much information.) For completeness sake, here it the RemoveAuthCookieAndRedirectToDefaultPage() method:

    private void RemoveAuthCookieAndRedirectToDefaultPage()

    {  
 
       FormsAuthentication.SignOut();

        Response.Redirect("/home/default.rails", true);

    }

One thing that you need to pay attention to here, we are not using the MonoRail API here, but the ASP.Net ones. This is because we want MonoRail to get the request with the current user already set, and this is still too early in the request pipeline for MonoRail to take over.

So, we now have a way to login a user, and a way to authenticate a logged in user on the next request, we need two more things before we can say that the security part of the application is complete. The first thing is to allow a user to log out, and to make sure that only authorized users can access the administrators section.

We'll start by creating the Logout functionality, since this one is very simple.

    public void Logout()

    {

        FormsAuthentication.SignOut();

        Redirect("home", "default");

    }

We remove the cookie from the user and redirect to the default page. Now, let's implement security for the administration section. At this point, all we want to achieve is to allow only authenticated users to access thos pages. We will do this with a Filter. A Filter is a wrapper around a request, which can do action for the request. Here is our authentication filter:

public class AuthenticationFilter : IFilter

{

    public bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller)

    {

        if (context.CurrentUser == null || context.CurrentUser.Identity.IsAuthenticated == false)

        {

            context.Response.Redirect("home", "default");

            return false;

        }

        return true;

    }

}

It simply checks to see if you are authenticated, and if not, kicks you back to the home page. Now we need to integrate it with the AdminController, this is done by specifying this attribute:

[Filter(ExecuteEnum.BeforeAction, typeof(AuthenticationFilter))]

public class AdminController : MythicalBugTrackerControllerBase

This is it, we have now secured the administration section, only authenticated users will be able to access those parts now. As I mentioned, this is reasonable security, but if we deal with sensitive data, or mission critical operations, it's not good enough. In those cases, you want to run the sensitive sections of your site on SSL, and have a full security audit before you go live.

Well, that was a long one. I hope that you're following, since there are still more to come (here is the non-complete, totally random list):

  • Tests! Tests! Tests!
  • Personalization
  • Roles
  • Handling exceptions
  • Actually writing the Submit Bug page.
  • Complex searches using Active Record
  • Mitigating Cross Site Scripting Attacks
  • Sending Emails
  • Thinking about Castle

In the meantime, I realized that I forgot to give the urls for you to check what is happening. I'm sure you figured them out, but here are the usable entry points into the application, for reference:

  • http://localhost:8080/admin/Projects.rails
  • http://localhost:8080/admin/users.rails
  • http://localhost:8080/admin/index.rails
  • http://localhost:8080/home/default.rails

You can get the application here.

Comments

No comments posted yet.

Comments have been closed on this topic.