Sunday, September 18, 2011

Using cookies to control return page after login on asp.net mvc 3

Default behavior and the scenario

On asp.net in general, when an user tries to access a page that requires the user to login first, it is fairly common to use the built in support to return to that page after the user logins.

When an user tries to access the page and an authorization made by the code determines the user doesn’t have access, a 401 is returned by that code. When that happens the FormsAuthenticationModule intercepts it and instead redirects the user to the login page configured in the asp.net config.

When the redirect to the login page is sent,  it includes a returnUrl query string parameter. On a successful the user is sent back to that url. If no return url is indicated, the user is sent to a default url after authentication.

The above works fine, if the scenario fits well with the site.

One consideration is with the use of register and forgot password options, in which case extra code needs to be put in place if you want the user to be redirected to the url visited when the process started. A similar case is if the user visits any other page linked there, like a faq or about page and then hitting the login option you almost certainly have in your master page/layout.

Another consideration comes with the user of ajax based login forms accessible anywhere on the site. Depending on the site, the user might be on a page where you don’t want it redirect to. You still might want it redirected to a page the user visited before doing some action, like in a client’s scenario, a specific deal page.

The cookie based approach

One way to handle the above scenario is by storing the returnUrl in a cookie. By doing so the user can visit other pages (like the register/forgot password options), and the redirect will go to the last relevant page. Additionally you can control which actions are considered relevant pages, in case the user uses the login option in your master page/layout in any random page they happen to be at.

To set the cookie in an unauthorized access scenario, one could use a module based approach just like the FormsAuthenticationModule. In our case, we instead introduced an AuthorizeWithReturnCookieAttribute, and replaced the use of [Authorize] with [AuthorizeWithReturnCookie].

  1. public class AuthorizeWithReturnCookieAttribute : AuthorizeAttribute
  2. {
  3.     protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
  4.     {
  5.         var ctx = filterContext.HttpContext;
  6.         NavigationCookies.SetReturnAfterAuthenticationUrl(ctx, ctx.Request.RawUrl);
  7.         base.HandleUnauthorizedRequest(filterContext);
  8.     }
  9. }

To set the cookie when a relevant page was visisted, we introduced a separate attribute ReturnAfterAuthenticationAttribute. Unlike the authorize attribute, this one is used on public pages, we want the user redirected back to if the login option in the master page/layout is used.

  1. public class ReturnAfterAuthenticationAttribute : ActionFilterAttribute
  2. {
  3.     public const string ViewDataIgnoreKey = "IgnoreReturnAfterAuthentication";
  4.     public override void OnActionExecuted(ActionExecutedContext filterContext)
  5.     {
  6.         var ctx = filterContext.HttpContext;
  7.         if ((bool?)filterContext.Controller.ViewData[ViewDataIgnoreKey] == true) return;
  8.         if (ctx.User.Identity.IsAuthenticated) return;
  9.         if (filterContext.HttpContext.Request.HttpMethod != "GET") return;
  10.         NavigationCookies.SetReturnAfterAuthenticationUrl(ctx, ctx.Request.RawUrl);
  11.     }
  12. }

There are a few more things going on in the above attribute.

  • We can avoid it being set based on logic executed in the action method, by setting true on the corresponding value in ViewData. In retrospective ctx.Items is probably the right place for it.
  • No point in setting the cookie if the user is already authenticated.
  • Post requests are not tracked.

For a more complete sample, the modified logon and register methods of the asp.net mvc template below. It still supports the returnUrl and gives it precendence (not what we did in our case, as we only use the cookie one).

  1. [HttpPost]
  2. public ActionResult LogOn(LogOnModel model, string returnUrl)
  3. {
  4.     if (ModelState.IsValid)
  5.     {
  6.         if (Membership.ValidateUser(model.UserName, model.Password))
  7.             return SetAuthCookieAndRedirect(model.UserName, model.RememberMe, returnUrl);
  8.         ModelState.AddModelError("", "The user name or password provided is incorrect.");
  9.     }
  10.     // If we got this far, something failed, redisplay form
  11.     return View(model);
  12. }
  1. [HttpPost]
  2. public ActionResult Register(RegisterModel model)
  3. {
  4.     if (ModelState.IsValid)
  5.     {
  6.         // Attempt to register the user
  7.         MembershipCreateStatus createStatus;
  8.         Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);
  9.  
  10.         if (createStatus == MembershipCreateStatus.Success)
  11.             return SetAuthCookieAndRedirect(model.UserName);
  12.         ModelState.AddModelError("", ErrorCodeToString(createStatus));
  13.     }
  14.     // If we got this far, something failed, redisplay form
  15.     return View(model);
  16. }
  1. ActionResult SetAuthCookieAndRedirect(string userName, bool createPersistentCookie=false, string returnUrl=null)
  2. {
  3.     FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
  4.     var url = IsValidReturnUrl(returnUrl) ?
  5.         returnUrl :
  6.         NavigationCookies.GetReturnAfterAuthenticationUrl(Request);
  7.     if (string.IsNullOrEmpty(url)) return RedirectToAction("Index", "Home");
  8.     return new RedirectResult(url);
  9. }
  10. private bool IsValidReturnUrl(string returnUrl)
  11. {
  12.     return Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
  13.            && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\");
  14. }

The NavigationCookies class just sets/gets the cookie:

  1. public class NavigationCookies
  2. {
  3.     private const string ReturnAfterAuthenticationKey = "ReturnAfterAuthentication";
  4.     public static void SetReturnAfterAuthenticationUrl(HttpContextBase context, string url)
  5.     {
  6.         url = VirtualPathUtility.ToAbsolute(url, context.Request.ApplicationPath);
  7.         var cookie = context.Response.Cookies[ReturnAfterAuthenticationKey];
  8.         cookie.Expires = DateTime.Now.AddDays(1);
  9.         cookie.Value = url;
  10.     }
  11.     public static string GetReturnAfterAuthenticationUrl(HttpRequestBase request)
  12.     {
  13.         var cookie = request.Cookies["ReturnAfterAuthentication"];
  14.         if (cookie == null || cookie.Expires > DateTime.Now) return String.Empty;
  15.         return cookie.Value;
  16.     }
  17. }

I added a full sample that uses the asp.net mvc 3 template to the following github repository: https://github.com/eglasius/Cookie-Based-ReturnUrl-After-Authentication-Sample. You can download a zip file of it directly at: https://github.com/eglasius/Cookie-Based-ReturnUrl-After-Authentication-Sample/zipball/master.

Monday, October 18, 2010

Adding sharing buttons for all your blogger posts

The layout I’ll show in this blog post includes sites that are related to software development, but this can be easily be modified by removing them or replacing them with other services appropriate to the specific particular blog.

Open your blogger dashboard and select the design option for your blog. Use the Download Full Template option in Edit HTML to save a copy of your current template. This is important to make sure you can go back to your current template if anything goes wrong.

In the Edit Template section select Expand Widget Templates. Search for post-footer-line in the textbox that contains your template. These corresponds to the lines with the built-in blogger sharing options and labels. I used post-footer-line-3 (was empty) and added another one (post-footer-line-4).

Following the html to share on DZone, DotNetShoutout, DotNetKicks, Hacker News, Digg, Reddit, Google Buzz and Yahoo Buzz:

<div class='post-footer-line post-footer-line-3'>
<b:if cond='data:blog.pageType == &quot;item&quot;'>
<span style='top: 5px; position:relative; '>
<script type='text/javascript'>
var dzone_url = '<data:post.url/>';
var dzone_title = '<data:post.title/>';
var dzone_style = '2';
</script>
<script language='javascript' src='http://widgets.dzone.com/links/widgets/zoneit.js'/>
</span>
<a expr:href='&quot;http://dotnetshoutout.com/Submit?url=&quot; + data:post.url + &quot;&amp;title=&quot; + data:post.title' expr:id='data:widget.instanceId + &quot;_shoutit&quot;' rel='nofollow' rev='vote-for'> <img alt='Shout it' expr:src='&quot;http://dotnetshoutout.com/image.axd?url=&quot; + data:post.url' style='border:0px; top: 2px; position:relative;'/></a>
<a expr:href='&quot;http://www.dotnetkicks.com/submit/?url=&quot; + data:post.url + &quot;&amp;title=&quot; + data:post.title' expr:id='data:widget.instanceId + &quot;_kickit&quot;' rel='nofollow'><img alt='Submit this story to DotNetKicks' expr:src='&quot;http://www.dotnetkicks.com/Services/Images/KickItImageGenerator.ashx?url=&quot; + data:post.url'/></a>
<a expr:href='&quot;http://news.ycombinator.com/submitlink?u=&quot; + data:post.url + &quot;&amp;t=&quot; + data:post.title' rel='nofollow'><img src='http://ycombinator.com/images/y18.gif' title='Submit to Hacker News'/></a>
</b:if>
</div>
<div class='post-footer-line post-footer-line-4'>
<b:if cond='data:blog.pageType == &quot;item&quot;'>
<script type='text/javascript'>
(function() {
var s = document.createElement('SCRIPT'), s1 = document.getElementsByTagName('SCRIPT')[0];
s.type = 'text/javascript';
s.async = true;
s.src = 'http://widgets.digg.com/buttons.js';
s1.parentNode.insertBefore(s, s1);
})();
</script>
<span style='top: 7px; position:relative; margin-right: 5px;'>
<a class='DiggThisButton DiggCompact'/>
</span>
<script>
reddit_url='<data:post.url/>';
reddit_title='<data:post.title/>';
</script>
<span style='top: 10px; position:relative; margin-right: 5px;'>
<script language='javascript' src='http://reddit.com/button.js?t=1'/>
</span>
<a class='google-buzz-button' data-button-style='small-count' href='http://www.google.com/buzz/post' rel='nofollow' title='Post to Google Buzz'/>
<script src='http://www.google.com/buzz/api/button.js' type='text/javascript'/>
<script type='text/javascript'>
yahooBuzzArticleHeadline = '<data:post.title/>';
yahooBuzzArticleSummary = '<data:post.title/>';
yahooBuzzArticleCategory = 'science';
yahooBuzzArticleType = 'text';
yahooBuzzArticleId = <data:post.url/>;
</script>
<span style='display: inline-block;'>
<script badgetype='text-votes' src='http://d.yimg.com/ds/badge2.js' type='text/javascript'/>
</span>
</b:if>
</div>

Special blogger if at lines 2 and 17 make sure we only display the various share options on the actual post pages. Some of the share code above uses the current page’s URL, so if we wanted to share options in the list of blog posts in the home page of the blog we would need to use alternate sharing code and move the general script files to the blog template.


Spans with relative positioning at lines 3, 27, 34 were added to align the corresponding share buttons.


The span at line 46 makes sure the Yahoo Buzz button code is contained in a block element. This fixes an issue on IE, where the image shows way above the rest of the text.


The line 42 sets the Yahoo Buzz category to science, you might want to use a different category.


Below some blog posts used as referenced for some of the share code:


Sunday, October 17, 2010

Disambiguating a method during a .net library upgrade

One scenario I had recently, involved a method that was named in such a way that it wasn’t clear the effect it would have given the context. Of course, it came to attention because it was used expecting a different behavior. The upside is that it was detected during a focused integration test, so it was revealed very early in the involved scenario.

As the library is used in more than one project, this raised the question if the code could have been used in such way in any of the other projects. We decided on changing it so we could tell if this was the case during the build of any project that used the library.

Below code sample outlines the approach we used to update the library so developers involved review the usages to the now clearly named alternatives.

[Obsolete("Use ActionA or ActionB", true)]
public static void ActionThatWasntClearIfItDidAorB()
{
ActionA();
}
public static void ActionA()
{
// original code
}
public static void ActionB()
{
// alternative code
}

Friday, October 15, 2010

Adding reCAPTCHA with JavaScript

In some dynamic interfaces, you may not want to load reCAPTCHA with the initial page loaded. In others you can’t because it would expire by the time the user uses that piece of the UI.

In the above scenarios you can add reCAPTCHA just when you need it.

First include the recaptcha script:

<script type="text/javascript" src='http://www.google.com/recaptcha/api/js/recaptcha_ajax.js'></script>
If the page is served over https make sure to use that in the above url. Alternatively do it dynamically, like in asp.net MVC you can do:
<script type="text/javascript" src='<%= Request.Url.Scheme + 
"://www.google.com/recaptcha/api/js/recaptcha_ajax.js" %>'></script>
Add an empty div where you’ll want the reCAPTCHA to appear in the HTML.
In the corresponding JavaScript action, do:
Recaptcha.create(theRecaptchaPublicKey, targetDiv);
If you are using jQuery, one way is to retrieve it based on a css class like:
$('#someContainer .captcha').each(function () {
Recaptcha.create(theRecaptchaPublicKey, this);
});

Thursday, October 14, 2010

Adding reCAPTCHA to asp.net MVC

These are just a few steps, as the latest reCAPTCHA library has all the pieces you need for asp.net MVC:

  • Go to Get reCAPTCHA, which gives you the public and private keys you need to use reCAPTCHA with your site
  • Download the latest .net library at: http://code.google.com/p/recaptcha/downloads/list. Currently: dotnet-1.0.4.0
  • Add <%= Html.GenerateCaptcha()%> to the form you want to protect with the reCAPTCHA. Naturally you have to add the namespace for the extension to be recognized, some of the options:
    • Add the namespace to the web.config:
      <pages>
      <namespaces>
      ...
      <add namespace="Recaptcha" />

    • Add the namespace at the view:

      <%@ Import Namespace="Recaptcha" %>

    • Add your own extension method that wraps it. I changed the signature to return MvcHtmlString to prevent double encoding when using it with “<%:” instead of “<%=”

      public static MvcHtmlString GenerateCaptcha(this HtmlHelper htmlHelper)
      {
      var html = Recaptcha.RecaptchaControlMvc.GenerateCaptcha(htmlHelper);
      return MvcHtmlString.Create(html);
      }

  • Add the RecaptchaControlMvc.CaptchaValidator attribute to your controller. Also add parameters named captchaIsValid and captchaErrorMessage. Just like:

    [RecaptchaControlMvc.CaptchaValidator]
    public ActionResult MyMethod(Something else, bool captchaValid, string captchaErrorMessage)
    {
    // do something if (!captchaValid)
    }

  • Configure your keys. Some options:

    • Add to appsettings in the web.config, with entries named: RecaptchaPublicKey and RecaptchaPrivateKey
    • Set at Application Start:

      RecaptchaControlMvc.PrivateKey = privKey;
      RecaptchaControlMvc.PublicKey = pubKey;

Wednesday, October 13, 2010

Protecting asp.net machine keys and connection strings

Last month I blogged about how the asp.net padding oracle vulnerability related to getting different levels of access to the application, where part of it involved gaining access to unprotected machine keys at the web.config of the affected sites.

While the Microsoft’s patch that closes the vulnerability is already on Windows Update and other distribution channels, it doesn’t mean we shouldn’t pay attention to keeping important access information of our application out of harms way.

Protecting config sections

One way to protect machine keys, connection strings and other sensitive information in the web.config is to use protected sections. Check Microsoft’s Walkthrough: Encrypting Configuration Information Using Protected Configuration, for steps to use them in your application. By using this approach, the information is no longer in clear text in the web.config, and is encrypted with a key that’s not anywhere in the web site’s folder.

Machine’s level web.config

If the server isn’t shared with third party applications, another option is to configure the machine key at the machine level’s web.config. This is another way the machine key wouldn’t have been exposed in the event access to the site’s folder was gained, like in the recent vulnerability.

For .net 4, look it at: %systemroot%\Microsoft.NET\Framework\v4.0.30319\Config\. Note that for .net 3 and 3.5, it uses the same config than the 2.0 framework.

Tuesday, October 12, 2010

NuPack Visual Studio Add Package References Crash and TypeLoadException

This is about a couple issues I found just after deciding to give NuPack a go, and what to do if you see them.

Add Package References - unloaded projects

After installing, I tried the Add Package References on an existing small solution. There it crashed. This wasn’t the same scenario described at Yet Another Debugging Tale – Visual Studio Disappearing, since the classic Solution Explorer was used and at least I got the closing notification during the crash.

A quick look at the solution I had opened revealed the likely cause: a couple projects in the solution that could not be loaded. So I quickly confirmed it, removed those projects and tried again, problem gone. Bug reported.

Package Manager Console reports TypeLoadException

When opening the console you see the below:

System.TypeLoadException: Could not load type 'System.Management.Automation.Runspaces.InitialSessionState' from assembly 'System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'.
at NuPackConsole.Host.PowerShell.Implementation.PowerShellHostProvider.CreateHost(IConsole console)
at NuPackConsole.Implementation.PowerConsole.HostInfo.get_WpfConsole()
at NuPackConsole.Implementation.PowerConsoleToolWindow.get_WpfConsole()System.InvalidOperationException: Can't start ConsoleDispatcher. Host is null.
at NuPackConsole.Implementation.Console.ConsoleDispatcher.Start()
at NuPackConsole.Implementation.PowerConsoleToolWindow.MoveFocus(FrameworkElement consolePane)


The trace is a big tell here, since it mentions PowerShell right there. Searched and installed Power Shell, and I had an operational console. Specifically I used: Windows Management Framework Core package which contains PowerShell 2.