Book Image

Instant Nancy Web Development

By : Christian Horsdal
Book Image

Instant Nancy Web Development

By: Christian Horsdal

Overview of this book

Nancy is a C# web framework which aims to provide you ,the application developer, with an easy path to follow, where things will naturally fall into place and work. Nancy is a powerful, flexible, and lightweight web framework that allows you to get on with your job. Instant Nancy Web Development will give Readers practical introduction to building, testing, and deploying web applications with Nancy. You will learn how to take full advantage of Nancy to build clean application code, and will see how this code lends itself nicely to test driven development. You will also learn how to hook into Nancy to easily extend the framework. Instant Nancy Web Development offers you an in-depth exploration of all the major features of the Nancy web framework, from basic routing to deployment in the Cloud, and from model binding to automated tests. You will learn how to build web applications with Nancy and explore how to build web sites using Razor views. Next, you will learn how to build web based APIs suitable for JavaScript clients, mobile clients, and even desktop applications. In fact, you will learn how to easily combine the two into one. Finally, you will learn how to leverage Nancy to write clean and maintainable web applications quickly.
Table of Contents (7 chapters)

Authenticating users (Intermediate)


In this recipe, we extend the TodoNancy application to provide users with the option of logging in with Twitter. If users are logged in, they can read, modify, and delete their own Todos. Users—with the exception of anonymous users—will not be able to see each other's todos. Just like up until this point, any user can walk up to TodoNancy and add todos. If they are not authenticated, the application sees them as anonymous users and pools all their todos together in one big bucket belonging to the anonymous user.

Getting ready

As usual, open the code from the last recipe or grab it from the code download. Either way, you should be ready for the steps in this recipe as soon as you have the code from the last recipe.

How to do it...

The following steps help you add an authentication to your Nancy applications:

  1. First we introduce a simple User class in TodoNancy and along with it a special anonymous user:

    namespace TodoNancy
    {
      using System.Collections.Generic;
      using Nancy.Security;
    
      public class User : IUserIdentity
      {
        public static IUserIdentity Anonymous { get; private set; }
        static User()
        {
          Anonymous = new User { UserName = "anonymous" };
        }
    
        public string UserName { get; set; }
        public IEnumerable<string> Claims { get; private set; }
      }
    }
  2. Install the third-party NuGet package called Nancy.Authentication.WorldDomination. This package provides integration of the WorldDomination library into Nancy. WorldDomination provides an easy API towards third-party public authentication services, such as Facebook, Google, and Twitter. We are only going to take advantage of the Twitter part, but the code for setting up Google and Facebook login with WorldDomination is similar to that for Twitter.

  3. The WorldDomination Nancy integration works by providing handlers that take care of the communication with the third-party authentication services. These handlers can be reached simply by linking to them. In order to grant Twitter the access to log in in our application, we simply add the following line of code to Todos.cshtml:

        <a href="/authentication/redirect/twitter">Login with Twitter</a>
  4. When users click on this link, they are redirected to Twitter where they can log in and authorize our application to access their Twitter ID. When this process is done, WorldDomination calls back to a hook in our application code where we can do whatever application-specific stuff we need. In our case, we want to do two things: set a cookie containing an access token and redirect to "/". We specify these two things in a couple of tests that we put in a new class in TodoNancyTests called AuthenticationTests, which I will not go into detail with, since it has more to do with WorldDomination than with Nancy:

        [Fact]
        public void Should_redirect_to_root_on_social_authc_callback()
        {
          new Browser(new Bootstrapper()).Get("/testing");
          var callbackData = new AuthenticateCallbackData
          {
            AuthenticatedClient = new AuthenticatedClient("")
            {
              UserInformation = new UserInformation { UserName = "chr_horsdal" }
            }
          };
    
          var sut = new SocialAuthenticationCallbackProvider();
    
          var actual = sut.Process(TestingModule.actualModule, callbackData);
    
          Assert.Equal(HttpStatusCode.SeeOther, actual.StatusCode);
        }
    
        [Fact]
        public void Should_set_todo_user_cookie_on_social_authc_callback()
        {
          new Browser(new Bootstrapper()).Get("/testing");
          var userNameToken = new TokenService().GetToken("chr_horsdal");
          var callbackData = new AuthenticateCallbackData
          {
            AuthenticatedClient = new AuthenticatedClient("")
            {
              UserInformation = new UserInformation { UserName = "chr_horsdal" }
            }
          };
    
          var sut = new SocialAuthenticationCallbackProvider();
    
          var actual = (Response) sut.Process(TestingModule.actualModule, callbackData);
    
          Assert.Contains("todoUser", actual.Cookies.Select(cookie => cookie.Name));
          Assert.Contains(userNameToken, actual.Cookies.Select(cookie => cookie.Value));
        }
  5. To make these tests pass—well even just compile—add this class to TodoNancy:

      public class SocialAuthenticationCallbackProvider : IAuthenticationCallbackProvider
      {
        public dynamic Process(NancyModule module, AuthenticateCallbackData callbackData)
        {
          module.Context.CurrentUser = new User { UserName = callbackData.AuthenticatedClient.UserInformation.UserName };
          return module.Response
            .AsRedirect("/")
            .AddCookie(new NancyCookie("todoUser", new TokenService().GetToken(module.Context.CurrentUser.UserName)));
        }
    
        public dynamic OnRedirectToAuthenticationProviderError(NancyModule nancyModule, string errorMessage)
        {
          return "login failed: " + errorMessage;
        }
      }
  6. The key thing in the previous code is that the SocialAuthenticationCallbackProvider class implements the IAuthenticationCallbackProvider interface, which is defined by WorldDomination. WorldDomination automatically picks up the implementation of IAuthenticationCallbackProvider and uses it for the callbacks at the end of the authentication process. There is one callback used for error cases and one used for successful cases. In the successful cases, we pick the username out of the callback data, obtain an access token for that user, and set a cookie with the token. Finally, we redirect to the root of our application. The access token is obtained from a service that I'm not showing here. This service has the responsibility to provide secure tokens, authenticate them later on, to revoke them if necessary, and possibly time them out after a period of inactivity. The details of this are beyond the scope of this book. Nota Bene. The implementation of the TokenService in the code download is just the identity function, which is very insecure and should not be used in production.

  7. At this point, users can authenticate with Twitter and we store an access token in a cookie on their machine, but we forget all about that when they make their next request. So, the next step is to read the cookie on incoming requests and to set the current user based on the contents. The first test towards this is as follows:

        [Fact]
        public void Should_set_user_identity_when_cookie_is_set()
        {
          var expected = "chr_horsdal";
          var userNameToken = new TokenService().GetToken(expected);
          var sut = new Browser(new Bootstrapper());
    
          sut.Get("/testing", with => with.Cookie("todoUser", userNameToken));
    
          Assert.Equal(expected, TestingModule.actualUser.UserName);
        }
  8. The previous test makes a request to /testing, which is a route picked up by a test module that we add to TodoNancy testing. The testing module remembers the current user when it receives a request:

      public class TestingModule : NancyModule
      {
        public static IUserIdentity actualUser;
        public TestingModule()
        {
          Get["/testing"] = _ =>
          {
            actualUser = Context.CurrentUser;
            return HttpStatusCode.OK;
          };
        }
      }
  9. As an exercise, add a similar test that checks that the current user is set to the anonymous user when no todoUser cookie is set.

  10. To set the current user on incoming requests, we add a Before action by calling the following method in the Bootstrapper.ApplicationStartup method:

        private static void SetCurrentUserWhenLoggedIn(IPipelines pipelines)
        {
          pipelines.BeforeRequest += context =>
          {
            if (context.Request.Cookies.ContainsKey("todoUser"))
              context.CurrentUser = new TokenService().GetUserFromToken(context.Request.Cookies["todoUser"]);
            else
              context.CurrentUser = User.Anonymous;
            return null;
          };
        }
  11. This just looks at the request to see if there is a "todoUser" cookie. If so, the TokenService method is asked to resolve the access token to a user, otherwise the anonymous user is used. Note that anything we set on NancyContext in a Before action is carried through to the handler and any After actions.

  12. The user can now log in and we remember the log in for as long as the access token is valid. We are still not doing anything based on the current user though. What we want to do is to let the user have a private set of todos. To spec this out, we need to add tests for HTTP requests to /todos using the GET, POST, PUT, and DELETE verbs and using different combination of usernames. For the sake of brevity, I have only included one such test here. The rest I am sure you can easily write at this point:

      public class UserSpecificTodosTests
      {
        private const string UserName = "Alice";
        private readonly Browser sut;
        private readonly IDataStore fakeDataStore;
    
        public UserSpecificTodosTests()
        {
          fakeDataStore = A.Fake<IDataStore>();
          sut = new Browser(with =>
          {
            with.Module<TodosModule>();
            with.ApplicationStartup((container, pipelines) =>
            {
              container.Register(fakeDataStore);
              pipelines.BeforeRequest += ctx =>
              {
                ctx.CurrentUser = new User { UserName = UserName };
                return null;
              };
            });
          });
        }
    
        [Theory]
        [InlineData(0, 0, 0)][InlineData(0, 10, 0)][InlineData(0, 0, 10)][InlineData(0, 10, 10)]
        [InlineData(1, 0, 0)][InlineData(1, 10, 0)][InlineData(1, 0, 10)][InlineData(1, 10, 10)]
        [InlineData(42, 0, 0)][InlineData(42, 10, 0)][InlineData(42, 0, 10)][InlineData(42, 10, 10)]
        public void Should_only_get_user_own_todos(int nofTodosForUser, int nofTodosForAnonymousUser, int nofTodosForOtherUser)
        {
          var todosForUser = Enumerable.Range(0, nofTodosForUser).Select(i => new Todo { id = i, userName =  UserName });
          var todosForAnonymousUser = Enumerable.Range(0, nofTodosForAnonymousUser).Select(i => new Todo { id = i });
          var todosForOtherUser = Enumerable.Range(0, nofTodosForOtherUser).Select(i => new Todo { id = i, userName = "Bob" });
    
          A.CallTo(() => fakeDataStore.GetAll())
          .Returns(todosForUser.Concat(todosForAnonymousUser).Concat(todosForOtherUser));
    
          var actual = sut.Get("/todos/", with => with.Accept("application/json"));
    
          var actualBody = actual.Body.DeserializeJson<Todo[]>();
          Assert.Equal(nofTodosForUser, actualBody.Length);
        }
  13. These tests drive changes to our TodosModule, the IDataStore interface, and the MongoDatastore class such that the module inspects the current user, and the module and data store in concert provide access to only the users own todos. The IDatastore interface becomes as follows:

      public interface IDataStore
      {
        IEnumerable<Todo> GetAll();
        long Count { get; }
        bool TryAdd(Todo todo);
        bool TryRmove(int id, string userName);
        bool TryUpdate(Todo todo, string userName);
      }
  14. And the TodosModule class becomes as follows (the changes are highlighted):

      public class TodosModule : NancyModule
      {
        public TodosModule(IDataStore todoStore) : base("todos")
        {
          Get["/"] = _ =>
            Negotiate
            .WithModel(todoStore.GetAll().Where(todo => todo.userName == Context.CurrentUser.UserName).ToArray())
            .WithView("Todos");
    
          Post["/"] = _ =>
          {
            var newTodo = this.Bind<Todo>();
            newTodo.userName = Context.CurrentUser.UserName;
            if (newTodo.id == 0)
              newTodo.id = todoStore.Count + 1;
    
            if (!todoStore.TryAdd(newTodo))
              return HttpStatusCode.NotAcceptable;
    
            return Negotiate.WithModel(newTodo)
                            .WithStatusCode(HttpStatusCode.Created)
                            .WithView("Created");
          };
    
          Put["/{id}"] = p =>
          {
            var updatedTodo = this.Bind<Todo>();
            updatedTodo.userName = Context.CurrentUser.UserName;
            if (!todoStore.TryUpdate(updatedTodo, Context.CurrentUser.UserName))
              return HttpStatusCode.NotFound;
    
            return updatedTodo;
          };
    
          Delete["/{id}"] = p =>
          {
            if (!todoStore.TryRmove(p.id, Context.CurrentUser.UserName))
              return HttpStatusCode.NotFound;
    
            return HttpStatusCode.OK;
          };
        }
      }

How it works...

Most of what we did in the preceding steps we have seen before. The main new part is the WorldDomination library. Simply by installing the Nancy.Authentication.WorldDomination NuGet package, we got a few new routes and handlers in our Nancy application. WorldDomination achieves this by taking advantage of the way Nancy discovers modules. Nancy scans all loaded assemblies for implementations of INancyModule. This scanning includes third-party assemblies, so the Nancy integration in WorldDomination simply includes Nancy modules, which then automatically become part of our application.

There's more...

Nancy also supports authentication controlled directly in the application. For instance, there is a NuGet package that provides forms authentication, and there is another NuGet supporting stateless authentication schemes.

For modules that we only want to allow authenticated users to access, Nancy provides a convenience method; just add the following line of code to the constructor and all unauthenticated requests to the module are denied access:

      this.RequiresAuthentication();

Similarly, if you only want to allow HTTPS requests, just add the following line of code:

      this.RequiresHttps();

Note that the User class we previously introduced includes a list of claims. We did not take advantage of these but it shows that Nancy supports adding claims-based authorization. We just have to make the TokenService type provide a list of claims for the user and then check them in the modules. Nancy provides even more convenient methods for checking claims. For instance, RequiresClaims accepts a list of claims. If the current user does not have those claims, the access is denied.