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.
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.
The following steps help you add an authentication to your Nancy applications:
First we introduce a simple
User
class inTodoNancy
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; } } }
Install the third-party NuGet package called
Nancy.Authentication.WorldDomination
. This package provides integration of theWorldDomination
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.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>
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 inTodoNancyTests
calledAuthenticationTests
, 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)); }
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; } }
The key thing in the previous code is that the
SocialAuthenticationCallbackProvider
class implements theIAuthenticationCallbackProvider
interface, which is defined by WorldDomination. WorldDomination automatically picks up the implementation ofIAuthenticationCallbackProvider
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 theTokenService
in the code download is just the identity function, which is very insecure and should not be used in production.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); }
The previous test makes a request to
/testing
, which is a route picked up by a test module that we add toTodoNancy
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; }; } }
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.To set the current user on incoming requests, we add a
Before
action by calling the following method in theBootstrapper.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; }; }
This just looks at the request to see if there is a
"todoUser"
cookie. If so, theTokenService
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 aBefore
action is carried through to the handler and anyAfter
actions.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 theGET
,POST
,PUT
, andDELETE
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); }
These tests drive changes to our
TodosModule
, theIDataStore
interface, and theMongoDatastore
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. TheIDatastore
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); }
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; }; } }
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.
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.