Web API & HTTP Basic Authentication
ASP.NET Web API is a framework for easily creating web services (http). Securing these services is a common need. There are many ways to do it. In this article we will implement basic HTTP authentication (RFC 2617).
#HTTP Basic Authentication
To authenticate someone, there are three methods:
- What you know, for example a couple username / password
- What you have, for example a smart card
- What you are, for example the recognition of the iris
The basic authentication method uses a username/password and is therefore the first type of authentication. Its operation is very simple:
When the user tries to access the resource without being authenticated, the server returns status 401 (Unauthorized) and the following header:
WWW-Authenticate: Basic realm = "http://www.sample.com"
The client understands that he must authenticate with the Basic method and re-execute the same request but including the header:
Authorization: Basic TWV6aWFudG91OjEyMzQ1Ng==
TWV6aWFudG91OjEyMzQ1Ng==
is the username followed by :
and then its password, all encoded in base 64. The original value is Meziantou:123456
. The server can thus authenticate the client. This header must be sent to each request.
#Pipeline Web API
ASP.NET Web API consists of a pipeline for processing queries. This is notably composed of a series of DelegatingHandler. These handlers process incoming requests one after the other, but also the responses. A handler can either pass the request to the next handler, or return the result directly to the previous handler and stop the progress in the pipeline.
A handler can for example perform the following treatments:
- Read and modify the headers of the request
- Add a header to the answer
- Authenticate the user
- Validate a request before it reaches the controller
- Trace / Log queries and responses
You will find a more complete diagram on the official site: http://www.asp.net/posters/web-api/ASP.NET-Web-API-Poster.pdf
DelegatingHandler
can integrate into the pipeline at 2 levels:
- Globally to the API: they intercept all messages
- By route: they only intercept the messages intended for the route with which they are associated
#Implementation
We will create a DelegatingHandler that:
- At the entrance is trying to authenticate the user if the header is present
- At the output adds the header if the status of the response is 401 (Unauthorized)
public abstract class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic Realm=\"{0}\"";
protected BasicAuthMessageHandler()
{
}
protected BasicAuthMessageHandler(HttpConfiguration httpConfiguration)
{
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected virtual string GetRealm(HttpRequestMessage message)
{
return message.RequestUri.Host;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Process request
AuthenticationHeaderValue authValue = request.Headers.Authorization;
if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter) &&
string.Equals(authValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
{
// Try to authenticate user
IPrincipal principal = ValidateHeader(authValue.Parameter);
if (principal != null)
{
request.GetRequestContext().Principal = principal;
}
}
return base.SendAsync(request, cancellationToken) // Send message to the InnerHandler
.ContinueWith(task =>
{
// Process response
var response = task.Result;
if (response.StatusCode == HttpStatusCode.Unauthorized &&
!response.Headers.Contains(BasicAuthResponseHeader))
{
response.Headers.Add(BasicAuthResponseHeader,
string.Format(BasicAuthResponseHeaderValue, GetRealm(request)));
}
return response;
}, cancellationToken);
}
private IPrincipal ValidateHeader(string authHeader)
{
// Decode the authentication header & split it
var fromBase64String = Convert.FromBase64String(authHeader);
var lp = Encoding.Default.GetString(fromBase64String);
if (string.IsNullOrWhiteSpace(lp))
return null;
string login;
string password;
int pos = lp.IndexOf(':');
if (pos < 0)
{
login = lp;
password = string.Empty;
}
else
{
login = lp.Substring(0, pos).Trim();
password = lp.Substring(pos + 1).Trim();
}
return ValidateUser(login, password);
}
protected abstract IPrincipal ValidateUser(string userName, string password);
}
The ValidateUser
method is abstract and must be implemented according to your needs:
public class SampleBasicAuthMessageHandler : BasicAuthMessageHandler
{
protected override IPrincipal ValidateUser(string userName, string password)
{
if (string.Equals(userName, "Meziantou", StringComparison.OrdinalIgnoreCase) && password == "123456")
return new GenericPrincipal(new GenericIdentity(userName, "Basic"), new string[0]);
return null;
}
}
All that remains is to declare the handler:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add(new SampleBasicAuthMessageHandler());
app.UseWebApi(config);
}
The complete example is available on GitHub: Web API - Basic Authentication.
warn: With this type of authentication it is strongly recommended to use HTTPS to protect the identifiers that otherwise circulate in clear on the network.
Do you have a question or a suggestion about this post? Contact me!