FubuMVC: Authorization
In this post, I'll be going over how to write a custom authorization rule to keep users from being able to edit another user's blog post in a simple blogging application. I'll do this by plugging into the authorization facilities built into FubuMVC. When it comes to authorization, FubuMVC is very powerful in letting you setup extremely customizable rules for deciding who has access to what. Luckily, Joshua Arnold was able to help me get these rules setup and I'd like to document the process for everyone else.
Overview
- Come up with a convention to identify which action calls need authorization, and how to access the information we need from the input models
- Do the heavy lifting by writing an authorization policy to check and see if the user is authorized to do what they're trying to do
- Teach FubuMVC about the convention
- Tell FubuMVC to use the convention
Step 1: Come up with a convention
In order to figure out which routes/action calls need authorization, I'm going to implement an interface on my input models. This will not only mark them so I can look them up later, but also provide me with a generic way of accessing the properties that I'm going to be running my authorization code against. In this case, all I need from the models are the post id, so that's all that's required on the interface. It's also worth noting here that I have two input models. I use EditPostRequestModel for the http get action call (to get the post in order to display it for the user to edit), and the EditPostInputModel for the http post action call (which handles the saving of the post after it's been edited by the user.)
public interface IPostModel
{
int PostId { get; }
}
public class EditPostInputModel : IPostModel
{
public int PostId { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string Slug { get; set; }
}
public class EditPostRequestModel : IPostModel
{
public int PostId { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string Slug { get; set; }
}
Step 2: Do the heavy lifting
Now that I've marked which actions need authorization, I'm going to write an authorization policy so I can specify how that authorization needs to be done. In this case I want to verify that the post being requested belongs to the user that is requesting it. This is a simple example, but you can do any sort of complex checking here that you wanted (e.g. If it were possible that your users could be editors and edit other user's posts, that check would go here.)
For my simple case, we can just check that the user property on the requested post, is the same user as the user that's currently logged in. The CurrentUser dependency is what I use to get the currently logged in user. Use whatever your convention is to do that here, instead.
public class PostAuthorizationPolicy<TModel> : IAuthorizationPolicy
where TModel : class, IPostModel
{
private readonly IPostRepository _postRepository;
private readonly CurrentUser _currentUser;
public PostAuthorizationPolicy(IPostRepository postRepository, CurrentUser currentUser)
{
_postRepository = postRepository;
_currentUser = currentUser;
}
public AuthorizationRight RightsFor(IFubuRequest request)
{
var model = request.Get<TModel>();
if (model == null)
{
return AuthorizationRight.Deny;
}
var post = _postRepository.GetById(model.PostId);
if (post == null)
{
return AuthorizationRight.Deny;
}
return post.User.Id == _currentUser.UserId
? AuthorizationRight.Allow
: AuthorizationRight.Deny;
}
}
Step 3: Teach FubuMVC about the convention
So, I have my action calls setup that need authorization, and I've created a way to do that authorization, the next step is to tie the two together and tell FubuMVC how to apply my authorization policy.
FubuMVC has a DSL to do this, which can be accessed through the Authorization property on the behavior chain. The way this works is inside FubuMVC, after it has applied all of your conventions, it will check to see if there have been any policies added to the Authorization property on the behavior chain. If it finds that there has been, it will wrap the chain with an internal behavior called AuthorizationBehavior.
The way the AuthorizationBehavior works is by calling the authorization policies for the behavior and checking the return value of them. If any of the policies return AuthorizationRight.Deny, then the chain is stopped and IAuthorizationFailureHandler is called on to handle the erred request.
It's very important that FubuMVC sets this convention up after it sets up all of the custom conventions. This ensures that the AuthenticationBehavior gets run before anything else in the behavior chain.
What I want to do is add a policy to the Authorization property for all action calls which have an input type that implements my IPostModel interface.
Since I made the policy an open generic, I'll close that generic with the type that the action call is using. This is what allows the authorization policy to get the model out of the IFubuRequest (since things are keyed by type), which means I can check values specific to that particular request.
public class PostAuthorizationConvention : IConfigurationAction
{
public void Configure(BehaviorGraph graph)
{
graph
.Behaviors
.Where(c => typeof (IPostModel).IsAssignableFrom(c.InputType()))
.Each(chain => chain.Authorization
.AddPolicy(typeof (PostAuthorizationPolicy<>).MakeGenericType(chain.InputType())));
}
}
Step 4: Tell FubuMVC to use the convention
Now that everything is all setup and I've defined how my convention works, I just have to tell FubuMVC to use the convention. To do this, in my FubuRegistry, I just add the following line.
public class MyRegistry : FubuRegistry
{
public MyRegistry()
{
Policies.Add<PostAuthorizationConvention>();
}
}
Now when I start up the application and browse to /post/edit/:id, I get a 403 forbidden message if the id is for a post that isn't mine. If the post is mine, I'm allowed to pass right through to the rest of the chain.
Bonus!
What if I don't want to return a 403 forbidden message? I want to do something unique and special!
Remember when I said that the AuthorizationBehavior will call on IAuthorizationFailureHandler if any of the policies go wrong in order to handle the authorization error? Well, all you have to do is implement your own IAuthorizationFailureHandler and tell FubuMVC to replace the default implementation with your custom one, in your FubuRegistry.
public class CustomAuthorizationFailureHandler : IAuthorizationFailureHandler
{
private readonly IOutputWriter _writer;
private readonly IUrlRegistry _urlRegistry;
public CustomAuthorizationFailureHandler(IOutputWriter writer, IUrlRegistry urlRegistry)
{
_writer = writer;
_urlRegistry = urlRegistry;
}
public void Handle()
{
// Get the url to the login page, and redirect the user there!
var url = _urlRegistry.UrlFor(new LoginRequestModel());
_writer.RedirectToUrl(url);
}
}
// In the registry
public class MyRegistry : FubuRegistry
{
public MyRegistry()
{
Services(x => { x.ReplaceService<IAuthorizationFailureHandler, CustomAuthorizationFailureHandler>(); });
}
}
Now when I attempt to browse to /post/edit/:id, if I don't own the post, I get redirected to the login screen.