Supporting Multiple Authentication with Web API in the web hosted case

With the newly released Web API Beta bits ( https://www.asp.net/web-api), you can support multiple authentication from client. Say you want the client send both the username/password as well as Client Certificate for SSL.

For the web hosted scenario, you can register a custom HTTP module to do the authentication and convert those client credentials into some principal that your controller can later authorize. Here is some sample code. Dislaimer, the code is only for demoing purpose, and not for production.

Step 1: Write a HttpModule to authenticate and turn the client credential to some principal information.

Code Snippet

  1. public void Init(HttpApplication context)
  2. {
  3.     context.AuthenticateRequest += (s, args) =>
  4.     {
  5.         var currentContext = HttpContext.Current;
  6.  
  7.         if (!currentContext.Request.IsSecureConnection)
  8.         {
  9.             throw new HttpException(403, "SSL required for Basic Authentication");
  10.         }
  11.  
  12.         var authHeader = context.Request.Headers[HTTP_HEADER_AUTHORIZATION];
  13.  
  14.         if (String.IsNullOrEmpty(authHeader))
  15.         {
  16.             currentContext.Response.StatusCode = 401;
  17.             currentContext.Response.End();
  18.             return;
  19.         }
  20.  
  21.         Tuple<string, string> credentials = GetCredentials(authHeader);
  22.         IPrincipal principal;
  23.  
  24.         // you can manufacture princical based on client certificate
  25.         if (context.Request.ClientCertificate.Subject == "some subject" &&
  26.             credentials.Item1 == "username" && credentials.Item2 == "password")
  27.         {
  28.             // This is only for demoing purpose!!! Do not use this in production.
  29.             var identity = new GenericIdentity("username with some subject from client certificate");
  30.             principal = new GenericPrincipal(identity, new[] { "Administrators" });
  31.         }
  32.         else
  33.         {
  34.             // we failed to validate the username/password
  35.             currentContext.Response.StatusCode = 401;
  36.             currentContext.Response.End();
  37.             return;
  38.         }
  39.  
  40.         // otherwise, we are authenticated, set the current context
  41.         currentContext.User = principal;
  42.     };
  43.  
  44.     context.EndRequest += (s, args) =>
  45.     {
  46.         var currentContext = HttpContext.Current;
  47.  
  48.         if (currentContext.Response.StatusCode == 401)
  49.         {
  50.             currentContext.Response.StatusCode = 401;
  51.             currentContext.Response.AddHeader(
  52.                 "WWW-Authenticate", String.Format("Basic realm=\"{0}\"", "Home"));
  53.         }
  54.     };
  55. }
  56.  
  57. // parse the authHeader into username and password
  58. private static Tuple<string, string> GetCredentials(string authHeader)
  59. {
  60.     // strip out "basic"
  61.     var encodedUsernameAndPwd = authHeader.Substring(6).Trim();
  62.  
  63.     var encoding = Encoding.GetEncoding("iso-8859-1");
  64.     var usernameAndPwd = encoding.GetString(Convert.FromBase64String(encodedUsernameAndPwd));
  65.     var usernameAndPwdElements = usernameAndPwd.Split(new[] { ':' });
  66.  
  67.     return new Tuple<string, string>(usernameAndPwdElements[0], usernameAndPwdElements[1]);
  68. }

Step 2: Write a custom authorization filter to require certain principal.

Code Snippet

  1. public class RequireAdminAttribute : AuthorizationFilterAttribute
  2.     {
  3.         public override void OnAuthorization(HttpActionContext context)
  4.         {
  5.             // do authorization based on the principle.
  6.             IPrincipal principal = context.ControllerContext.Request.GetUserPrincipal();
  7.             if (principal == null || !principal.IsInRole("Administrators"))
  8.             {
  9.                 context.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
  10.             }
  11.         }
  12.     }

Step 3: Add the authorization attribute to certain method that you want to secure in your controller.

Code Snippet

  1. public class HomeController : ApiController
  2.     {
  3.         /// <summary>
  4.         /// Anyone can get
  5.         /// </summary>
  6.         /// <returns></returns>
  7.         public HttpResponseMessage Get()
  8.         {
  9.             return new HttpResponseMessage(HttpStatusCode.OK)
  10.             {
  11.                 Content = new StringContent("Default User")
  12.             };
  13.         }
  14.  
  15.         /// <summary>
  16.         /// Only Admin can POST
  17.         /// </summary>
  18.         /// <returns></returns>
  19.         [RequireAdmin]
  20.         public HttpResponseMessage Post()
  21.         {
  22.             return new HttpResponseMessage(HttpStatusCode.OK)
  23.             {
  24.                 Content = new StringContent("User Posted")
  25.             };
  26.         }
  27.     }

Via IIS, you can set the SSL setting to require SSL and require Client certificate.

Hope this helps. Self host cases work in a little bit different way that i will blog sometime later.