Microsoft Web API 2 brings a lot of improvements when dealing with security in HTTP based web services. In this post I will show how to use some of these features in conjunction with Sitecore to create secure HTTP based web services that works with Sitecore’s users and roles.
The earlier versions of Web API only had authorization filters which meant that both authentication (determining who the user is) and authorization (what the user is allowed to do) had to be handled using this one filter type and controlling the execution order of the filters was almost impossible which meant that most of the time both authentication and authorization had to be bundled into a single filter to ensure that the authentication was performed before resource authorization.
With Web API 2 a new authentication filter type that is executed before the authorization filters was introduced and with this new filter type the authentication anf authorization can now be logically separated.
So let’s get coding by first creating an authentication filter that will work with Sitecore. The goal of the filter will be to verify that an authenticated Sitecore context user exist and map the user details to a prinicipal that the Web API can work with natively.
using System; using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Filters; using System.Web.Http.Results; namespace SitecoreSecurity { // By deriving from the Attribute class our filter can easily be assigned to // either an entire Web API controller or to individual methods within the controller public class SitecoreAuthentication : Attribute, IAuthenticationFilter { public Boolean AllowMultiple { get { return false; } } public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { // Check to determine if a valid Sitecore user exist in the Sitecore context. // This is possible because all calls to our Web API controller will automatically // include all cookies set by the site which means that the Sitecore.Context.User // will be mapped if (Sitecore.Context.User != null && Sitecore.Context.User.IsAuthenticated) { List<Claim> claims = new List<Claim> { // Create the user name as a name claim new Claim(ClaimTypes.Name, Sitecore.Context.GetUserName()), }; // map all roles that the user is a member of as additional role claims claims.AddRange(Sitecore.Context.User.Roles.Select(role => new Claim(ClaimTypes.Role, role.Name))); // Create a claims identity and a claims principal that is set as the context principal ClaimsIdentity id = new ClaimsIdentity(claims, AuthenticationTypes.Password); ClaimsPrincipal principal = new ClaimsPrincipal(new[] { id }); context.Principal = principal; } // If a valid Sitecore user is not found a 401 unauthorized result is returned else { context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request); } return Task.FromResult(0); } // This method would normally be used to return an authentication challenge to the client. However since Sitecore // is based on forms authentication no valid challenge can be sent. public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { return Task.FromResult(context.Result); } } }
The authentication filter is pretty simple. Mainly a principal is created with the Sitecore user name and the roles that the user is a member of or a 401 response is returned if the Sitecore context user doesn’t exist or isn’t authenticated (an anonymous user). Normally in the ChallengeAsync method a 401 response would be sent back to the client with a www-authenticate header describing the scheme that the client should use to forward the requested credentials to the server for authentication. However the only two authentication schemes that are officially approved are basic and digest and since Sitecore uses the web forms authetication inherent to ASP.NET there really isn’t a lot we can send back to the client. At best we could send a Location header with the location of the login form but this is not an official response.
Now with our authentication filter complete we can use it to secure our web service controller and we can use it both at the class and method level.
using System; using System.Web.Http; namespace SitecoreSecurity { [SitecoreAuthentication] public class TestController : ApiController { public String Get() { return "Hello world"; } } }
using System; using System.Web.Http; namespace SitecoreSecurity { public class TestController : ApiController { [SitecoreAuthentication] public String Get() { return "Hello world"; } } }
Our web service will now only accept requests from authenticated Sitecore users. The attribute can also be used in combination with the authorize filters that are part of the standard Web API. For instance we can use the atribute AllowAnonymous to allow annonymous requests to single methods when our attribute is used at the class level. More importantly however we can use it in combination with the Authorize attribute from the Web API to introduce more fine grained access rules based on for instance roles.
using System; using System.Web.Http; namespace SitecoreSecurity { [SitecoreAuthentication] [Authorize(Roles = "sitecore\\Test Web API")] public class TestController : ApiController { public String Get() { return "Hello world"; } } }
In the above example only authenticated users who are members of the “Test Web API” role can call the method. This is pretty neat stuff but when working with Sitecore we are used to administrative users being able to bypass security and this will not be the case with the standard Authorize filter where you have to be a member of the specified role regardless of administrative privileges. We could create a new authorize filter that performs a check to see if the user has administrative privileges but this would not work as expected since the most restrictive authorize filter will always apply and that means that the filter that specifies that the user has to be a member of the specified role will take effect.
Don’t worry though this can easily be fixed by introducing our own authorize filter derived from the existing filter.
using System; using System.Web.Http; using System.Web.Http.Controllers; namespace SitecoreSecurity { public class SitecoreAuthorizeAttribute : AuthorizeAttribute { protected override Boolean IsAuthorized(HttpActionContext actionContext) { if (Sitecore.Context.User != null && Sitecore.Context.IsAdministrator) return true; return base.IsAuthorized(actionContext); } } }
The filter is pretty much self-explanatory. If the user is an administrator the filter will always return true otherwise the base filter will be called.
That is pretty much it. We now have an authentication filter that we can use in conjunction with the Sitecore security setup and a specialized authorize filter that will always grant access to users with administrative priviliges.
There is a single point however that needs to be emphasized. Sitecore resolves the context user from the cookies sent by the client and that means that the authentication filter inherently relies on these. This however entails that by using it our web service can no longer be called a true REST based web service as the “specifications” for a REST service specifies that all communication should be done through the standard HTTP protocol which means that the security scheme should also be based on authenticate HTTP headers.