Simple implementation of ASP.NET Web API Basic authentication security
Securing Web API with simple basic authentication and consuming it from the client code
Authentication in web services is a bit more different than with web pages because of one simple reason. There is no UI for entering credentials to authenticate to consume service. Credentials need to be supplied during the service call. This can be easily done through headers.
However, although it is really simple to implement basic authentication, it has one major disadvantage which is credentials are sent in plain text in every request (SSL is mandatory to encrypt requests)
Apart from this disadvantage it is pretty easy to use it especially if your client is an application because basic authentication is following standard (RFC 2617) and it is supported by many platforms and browsers.
The following implementation uses Web.config to store credentials for the sake of simplicity. Depending on your requirements you can use any authentication provider. Credentials keys in web config are following the pattern, so it is pretty easy to introduce new users
<appSettings> <add key="auth.realm" value="1562EEDB4C124DDC83E70FF3C86CA9B1"/> <add key="auth.user.User1" value="pass123"/> <add key="auth.user.MyUser" value="Pass321"/> </appSettings>
Realm value is also stored in Web.config since we will use single realm without any segmentation. More about realm value you can find at http://tools.ietf.org/html/rfc2617#section-1.2
Since we are following pattern for credentials we can easily read credentials from web.config with a simple LINQ expression
const String AUTH_USER_PREFIX= "auth.user."; static readonly IDictionary<String, String> logins = ConfigurationManager.AppSettings.AllKeys.Where(k => k.StartsWith(AUTH_USER_PREFIX)) .ToDictionary(key => key.Replace(AUTH_USER_PREFIX, String.Empty), value => ConfigurationManager.AppSettings.Get(value));
ConfigurationManager class is static, so we do not need to go and read values on every request. Instead we'll store credentials in a singletone instance of IDictionary<String,String> variable and use it for aech authenticated request.
Handling the authenticated and non authenticated requests
Now we need to handle request authentication in a custom IHttpModule implementation. Since we have our credentials in web.config file we only need to read headers and compare them with ones in a web.config
using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Net.Http.Headers; using System.Security.Principal; using System.Text; using System.Threading; using System.Web; namespace WebApi.BasicAuthentication.Modules { public class BasicAuthHttpModule : IHttpModule { const String AUTH_USER_PREFIX= "auth.user."; static readonly IDictionary<String, String> logins = ConfigurationManager.AppSettings.AllKeys.Where(k => k.StartsWith(AUTH_USER_PREFIX)) .ToDictionary(key => key.Replace(AUTH_USER_PREFIX, String.Empty), value => ConfigurationManager.AppSettings.Get(value)); public void Init(HttpApplication context) { context.AuthenticateRequest += OnAuthenticateRequest; context.EndRequest += OnEndRequest; } private static void OnAuthenticateRequest(object sender, EventArgs e) { var authHeaders = HttpContext.Current.Request.Headers["Authorization"]; if (authHeaders != null) { var authHeadersValue = AuthenticationHeaderValue.Parse(authHeaders); if (authHeadersValue.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) && !String.IsNullOrWhiteSpace(authHeadersValue.Parameter)) { try { var credentials = authHeadersValue.Parameter; var encoding = Encoding.GetEncoding("iso-8859-1"); credentials = encoding.GetString(Convert.FromBase64String(credentials)); string name = credentials.Split(':').First(); string password = credentials.Split(':').Last(); if (logins.Any(l => l.Key.Equals(name) && l.Value.Equals(password))) { //Set the principal for validated user var principal = new GenericPrincipal(new GenericIdentity(name), null); Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } } else { //Authentication failed HttpContext.Current.Response.StatusCode = 401; } } catch (FormatException) { HttpContext.Current.Response.StatusCode = 401; } } } } private static void OnEndRequest(object sender, EventArgs e) { var response = HttpContext.Current.Response; if (response.StatusCode == 401) { //Addh eaders if authentication failed response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", ConfigurationManager.AppSettings.Get("auth.realm"))); } } public void Dispose() { } } }
We have authentication module which will handle our request authentication but until we declare it in web.config it will not be picked up by ASP.NET runtime, so we register it in modules section in system.webServer section
<system.webServer> <modules> <add name="BasicAuthHttpModule" type="WebApi.BasicAuthentication.Modules.BasicAuthHttpModule, WebApi.BasicAuthentication"/> </modules> </system.webServer>
We still do not have authentication engaged and that is because we need to decorate our controller actions with [Authorize] attribute to trigger our custom authentication module.
using System.Net; using System.Net.Http; using System.Web.Http; namespace WebApi.BasicAuthentication.Controllers { public class DefaultController : ApiController { [Authorize] public HttpResponseMessage Get() { return Request.CreateResponse(HttpStatusCode.OK, "Hello from Default controller!"); } } }
Now if we try to access /api/default we will be prompted to authenticate ourself to get the response. You can try any of the username and password combination we have declared in our web.config file. Voila, we have our Web API protected with basic authentication.
Consuming Web API protected with Basic authentication
No to get the response from endpoint which is protected with basic security we need to inject our credentials into headers using same ISO-8859-1 and base64.
using System; using System.IO; using System.Net; using System.Text; namespace WebApi.BasicAuthentication.Client { class Program { static void Main(string[] args) { String username = "User1"; String password = "pass123"; String url = "http://localhost:59496/api/default"; String encoded = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}")); var request = WebRequest.Create(url); request.Headers.Add("Authorization", $"Basic {encoded}"); var response = request.GetResponse(); using (var streamReader = new StreamReader(response.GetResponseStream())) { Console.WriteLine(streamReader.ReadToEnd()); } Console.ReadLine(); } } }
Both Web API and client code are available from the download section of this article so you can download it and try it yourself.
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article