UDATE:
I’ve built a new version for the MVC Web API:
http://remy.supertext.ch/2012/04/basic-http-authorization-for-web-api-in-mvc-4-beta/
One should not believe it, but it seems that there is no official way to use your own version of Basic HTTP Authentication with the WCF Web API in an MVC Web Application yet. So, now that I’ve used all possible necessary keywords we can dive right in.
We are using a custom ASP.NET Membership provider and the REST API should work with a token over Basic HTTP Authentication (like Basecamp). So, the built in Windows Basic Authentication is not an option.
After scanning dozens of posts on Stackoverflow and other resources I realized that either I have to go with the WCF REST Contrib library or with Open Rasta. But since I already started with the WCF Web API Preview 6 to build my REST API (and it worked fine so far) I didn’t want to switch now.
The most promising solution I found was from jslaybaugh. He is basically using a custom version of the [Authorize] attribute from the normal MVC framework and somehow integrated it all with Ninject. For some reason I didn’t got it working. In general AuthorizeAttribute and action filters are MVC specific. For WCF we have the HttpOperationHandler or the DelegatingHandler. They have their specific uses and advantages. Some info about his from Glenn Block.
I’ve decided to go with the HttpOperationHandler and found a good example from Phil Haack where he implements a Role authorization Module, that also works with Attributes, so we can implement something very similar to the AuthorizeAttribute of MVC. There are simpler solutions, e.g. you could just check this inside your Controller, but Craig Stuntz has some good points about why this is a bad idea.
So, let’s look at the code (which is a potpourri of all the above examples):
[AttributeUsage(AttributeTargets.Method)] public class BasicHttpAuthorizationAttribute : Attribute { bool requireSsl = true; public bool RequireSsl { get { return requireSsl; } set { requireSsl = value; } } } |
With this simple BasicHttpAuthorizationAttribute class we can achieve the the attribute functionality. So we can use it like this:
[BasicHttpAuthorization(RequireSsl = true)] [WebGet(UriTemplate = "")] public IEnumerable Get() { |
The RequireSsl is just an example property, you could also do a role membership check in the same way.
But the most important part is the implementation of the HttpOperationHandler. We pass the BasicHttpAuthorizationAttribute as an argument.
There are three main points that deserve attention here:
- If the user is not authenticated yet or provides the wrong credentials we return a HttpResponseException in the OnHandle method. We set the status code to 401 and add the WWW-Authenticate = Basic header. This creates the functionality, where the browser asks for a username/password and then automatically resends the request.
- In ParseAuthHeader we get the username and password out of the request. You can then use this info with in your own way. For example with your own custom membership provider.
- If the user can access this method, we create a GenericPrincipal and assign it to HttpContext.Current.User. Afterwards you can then just use your normal MemberShip and RoleProvider like in every normal ASP.NET application.
Other than that, there is not much magic in here.
public class BasicHttpAuthorizationOperationHandler : HttpOperationHandler { BasicHttpAuthorizationAttribute basicHttpAuthorizationAttribute; public BasicHttpAuthorizationOperationHandler(BasicHttpAuthorizationAttribute authorizeAttribute) : base("response") { basicHttpAuthorizationAttribute = authorizeAttribute; } protected override HttpRequestMessage OnHandle(HttpRequestMessage input) { if (Authenticate(input)) { return input; } else { var challengeMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized); challengeMessage.Headers.Add("WWW-Authenticate", "Basic"); throw new HttpResponseException(challengeMessage); } } private bool Authenticate(HttpRequestMessage input) { if (basicHttpAuthorizationAttribute.RequireSsl && !HttpContext.Current.Request.IsSecureConnection && !HttpContext.Current.Request.IsLocal) return false; if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")) return false; string authHeader = HttpContext.Current.Request.Headers["Authorization"]; IPrincipal principal; if (TryGetPrincipal(authHeader, out principal)) { HttpContext.Current.User = principal; return true; } return false; } private bool TryGetPrincipal(string authHeader, out IPrincipal principal) { var creds = ParseAuthHeader(authHeader); if (creds != null) { if (TryGetPrincipal(creds[0], creds[1], out principal)) return true; } principal = null; return false; } private string[] ParseAuthHeader(string authHeader) { // Check this is a Basic Auth header if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null; // Pull out the Credentials with are seperated by ':' and Base64 encoded string base64Credentials = authHeader.Substring(6); string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' }); if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[0])) return null; // Okay this is the credentials return credentials; } private bool TryGetPrincipal(string userName, string password, out IPrincipal principal) { // this is the method that does the authentication // you can replace this with whatever logic you'd use, but proper separation would put the if (userName.Equals("remy@test.ch") && password.Equals("test")) { // once the user is verified, assign it to an IPrincipal with the identity name and applicable roles // Example: //principal = new GenericPrincipal(new GenericIdentity(userName), System.Web.Security.Roles.GetRolesForUser(userName)); principal = new GenericPrincipal(new GenericIdentity(userName), new string[] {"Admin", "User"}); return true; } else { principal = null; return false; } } } |
Last but not least, we need to hook up our HttpOperationHandler with the BasicHttpAuthorizationAttribute object and route. For this we create a custom WebApiConfiguration and use a class extension to do the wiring. Honestly, I’m not really sure what is going on here, but it works 🙂
public class ApiConfiguration : WebApiConfiguration { public ApiConfiguration() { EnableTestClient = true; RequestHandlers = (c, e, od) => { // TODO: Configure request operation handlers }; this.AppendAuthorizationRequestHandlers(); } } public static class ConfigExtensions { public static void AppendAuthorizationRequestHandlers(this WebApiConfiguration config) { var requestHandlers = config.RequestHandlers; config.RequestHandlers = (c, e, od) => { if (requestHandlers != null) { requestHandlers(c, e, od); // Original request handler } var authorizeAttribute = od.Attributes.OfType() .FirstOrDefault(); if (authorizeAttribute != null) { c.Add(new BasicHttpAuthorizationOperationHandler(authorizeAttribute)); } }; } } |
And we pass this configuration in the global.asax.cs to the route handler:
public static void RegisterRoutes(RouteCollection routes) { var config = new ApiConfiguration(); routes.Add(new ServiceRoute("example", new HttpServiceHostFactory() { Configuration = config }, typeof(ExampleAPI))); } |
That is it. You can download the example project here: BasicAuthenticationWithWcfWebAPI.zip
Please let me know if this works for you and specially, if you find ways to improve it.
5 Kommentare zu “Basic Authentication with WCF Web API Preview 6”
It looks like they’re going to merge the web api with MVC 4:
http://bartwullems.blogspot.com/2012/02/farewell-wcf-web-api-welcome-aspnet-web.html
[…] is done via normal Basic HTTP Authentication. Most other APIs just use a token or your username and password. We decided to work with a username […]
[…] little while ago I posted a solution to do Basic Http Authorization with the Web API Preview 6. Web API got then merged into the next ASP.NET MVC 4 Beta Release and in […]
How do i get this to work with windows authentication???
This code is outdated and was for the Alpha. Please have a look at http://remy.supertext.ch/2012/04/basic-http-authorization-for-web-api-in-mvc-4-beta/
There you have to replace AccountManagement.ApiLogin(username, password) with your Windows Authentication code.