This post shows you how to handle encrypted user credentials in a Web Api application and offer further security
by enforcing https for all REST api calls.
Step 1: Create a new Web Api application:
This is our web service that will need to authenticate encrypted user credentials.
Step 2: Add a class for handling encryption and decryption
Crypto.cs
using System; using System.Linq; using System.Security.Cryptography; using System.Text; namespace WebApiEncrypt.Models { public static class Crypto { private const string PrivateKey = "<RSAKeyValue><Modulus>s6lpjspk+3o2GOK5TM7JySARhhxE5gB96e9XLSSRuWY2W9F951MfistKRzVtg0cjJTdSk5mnWAVHLfKOEqp8PszpJx9z4IaRCwQ937KJmn2/2VyjcUsCsor+fdbIHOiJpaxBlsuI9N++4MgF/jb0tOVudiUutDqqDut7rhrB/oc=</Modulus><Exponent>AQAB</Exponent><P>3J2+VWMVWcuLjjnLULe5TmSN7ts0n/TPJqe+bg9avuewu1rDsz+OBfP66/+rpYMs5+JolDceZSiOT+ACW2Neuw==</P><Q>0HogL5BnWjj9BlfpILQt8ajJnBHYrCiPaJ4npghdD5n/JYV8BNOiOP1T7u1xmvtr2U4mMObE17rZjNOTa1rQpQ==</Q><DP>jbXh2dVQlKJznUMwf0PUiy96IDC8R/cnzQu4/ddtEe2fj2lJBe3QG7DRwCA1sJZnFPhQ9svFAXOgnlwlB3D4Gw==</DP><DQ>evrP6b8BeNONTySkvUoMoDW1WH+elVAH6OsC8IqWexGY1YV8t0wwsfWegZ9IGOifojzbgpVfIPN0SgK1P+r+kQ==</DQ><InverseQ>LeEoFGI+IOY/J+9SjCPKAKduP280epOTeSKxs115gW1b9CP4glavkUcfQTzkTPe2t21kl1OrnvXEe5Wrzkk8rA==</InverseQ><D>HD0rn0sGtlROPnkcgQsbwmYs+vRki/ZV1DhPboQJ96cuMh5qeLqjAZDUev7V2MWMq6PXceW73OTvfDRcymhLoNvobE4Ekiwc87+TwzS3811mOmt5DJya9SliqU/ro+iEicjO4v3nC+HujdpDh9CVXfUAWebKnd7Vo5p6LwC9nIk=</D></RSAKeyValue>" ; private const string PublicKey = "<RSAKeyValue><Modulus>s6lpjspk+3o2GOK5TM7JySARhhxE5gB96e9XLSSRuWY2W9F951MfistKRzVtg0cjJTdSk5mnWAVHLfKOEqp8PszpJx9z4IaRCwQ937KJmn2/2VyjcUsCsor+fdbIHOiJpaxBlsuI9N++4MgF/jb0tOVudiUutDqqDut7rhrB/oc=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>" ; private static readonly UnicodeEncoding Encoder = new UnicodeEncoding(); public static string Decrypt(string data) { var rsa = new RSACryptoServiceProvider(); var dataArray = data.Split(','); var dataByte = new byte[dataArray.Length]; for (var i = 0; i < dataArray.Length; i++) dataByte[i] = Convert.ToByte(dataArray[i]); rsa.FromXmlString(PrivateKey); var decryptedByte = rsa.Decrypt(dataByte, false); return Encoder.GetString(decryptedByte); } public static string Encrypt(string data) { var rsa = new RSACryptoServiceProvider(); rsa.FromXmlString(PublicKey); var dataToEncrypt = Encoder.GetBytes(data); var encryptedByteArray = rsa.Encrypt(dataToEncrypt, false).ToArray(); var length = encryptedByteArray.Length; var item = 0; var sb = new StringBuilder(); foreach (var x in encryptedByteArray) { item++; sb.Append(x); if (item < length) sb.Append(","); } return sb.ToString(); } } }
Step 3: Create a new Authentication filter
I have created a new folder with which to put any new filter classes:
Into this new Filters folder create a new class called BasicAuthenticationAttribute. This needs to inherit from AuthorizationFilterAttribute.
BasicAuthenticationAttribute.cs
using System; using System.Net; using System.Net.Http; using System.Security.Principal; using System.Text; using System.Threading; using System.Web.Http.Controllers; using System.Web.Http.Filters; namespace WebApiEncrypt.Filters { public class BasicAuthenticationAttribute : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { var authHeader = actionContext.Request.Headers.Authorization; if (authHeader != null) { var authenticationToken = actionContext.Request.Headers.Authorization.Parameter; var decodedAuthenticationToken = Encoding.UTF8.GetString(Convert.FromBase64String(authenticationToken)); var decryptedAuthenticationToken = Crypto.Crypto.Decrypt(decodedAuthenticationToken); var usernamePasswordArray = decryptedAuthenticationToken.Split(':'); var userName = usernamePasswordArray[0]; var password = usernamePasswordArray[1]; // Replace this with your own system of security / means of validating credentials var isValid = userName == "andy" && password == "password"; if (isValid) { var principal = new GenericPrincipal(new GenericIdentity(userName), null); Thread.CurrentPrincipal = principal; //actionContext.Response = // actionContext.Request.CreateResponse(HttpStatusCode.OK, // "User " + userName + " successfully authenticated"); return; } } HandleUnathorized(actionContext); } private static void HandleUnathorized(HttpActionContext actionContext) { actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); actionContext.Response.Headers.Add("WWW-Authenticate", "Basic Scheme='Data' location = 'http://localhost:"); } } }
Step 4: Ensure basic authentication filter is applied in Values controller
Insert wherever you need this authentication to be enforced – globally or per function.
ValuesController.cs
using System.Collections.Generic; using System.Web.Http; using WebApiEncrypt.Filters; namespace WebApiEncrypt.Controllers { [BasicAuthentication] public class ValuesController : ApiController { // GET api/values public IEnumerable<string> Get() { return new[] {"value1", "value2"}; } // GET api/values/5 public string Get(int id) { return "value"; } // POST api/values public void Post([FromBody] string value) { } // PUT api/values/5 public void Put(int id, [FromBody] string value) { } // DELETE api/values/5 public void Delete(int id) { } } }
Step 5: Add a require https authorization filter attribute
First add a new Add a require https authorization filter attribute class:
RequireHttpsAttribute.cs
using System; using System.Net; using System.Net.Http; using System.Text; using System.Web.Http.Controllers; using System.Web.Http.Filters; namespace WebApiEncrypt.Filters { public class RequireHttpsAttribute : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { var req = actionContext.Request; if (req.RequestUri.Scheme == Uri.UriSchemeHttps) return; var html = "<p>Https required.</p>"; if (req.Method.Method == "GET") { actionContext.Response = req.CreateResponse(HttpStatusCode.Found); actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html"); var uriBuilder = new UriBuilder(req.RequestUri) { Scheme = Uri.UriSchemeHttps, Port = 443 }; actionContext.Response.Headers.Location = uriBuilder.Uri; } else { actionContext.Response = req.CreateResponse(HttpStatusCode.NotFound); actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html"); } } } }
Then update WebApiConfig.cs to ensure the require https is enforced globally:
WebApiConfig.cs
using System.Web.Http; using WebApiEncrypt.Filters; namespace WebApiEncrypt { public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.Filters.Add(new RequireHttpsAttribute()); } } }
Step 6: Set the project properties
Step 7: Test
So in this example, the username:password combination I want to test is “andy:password”
Using the ‘Crypto’ facility, this would encrypt to:
63,127,47,68,12,186,31,83,59,235,103,58,43,65,231,137,16,139,161,134,225,181,241,74,187,20,159,69,37,132,133,181,162,61,251,86,107,5,133,121,228,3,226,112,249,134,85,253,131,103,218,145,237,174,238,89,175,95,220,36,177,7,173,170,208,199,43,243,125,58,164,235,143,139,180,42,142,240,36,167,164,164,191,147,242,145,234,120,159,66,61,76,40,21,88,68,167,42,24,4,20,115,130,141,63,145,82,154,171,193,247,214,183,2,147,236,25,123,6,71,96,112,207,91,89,152,29,168
And the Base 64 encoding of the encrypted text is :
MTEyLDUsNjYsOTYsMTMsNjksMjI4LDEzLDE2OSwyNDIsMjIyLDQ0LDIwNywyNDcsMTk4LDY1LDEyMiwxNTMsMjM4LDE1NSw2MCwxMiw1MCwxMzcsMTM3LDQ1LDE2MSwxODQsMzIsNzIsMjAsMjMxLDIxMSw5OSwxMzMsMTYxLDE2LDE0Myw1MCwxMjgsNjQsMTgzLDEzMyw2NCw4NiwyMDEsMjUwLDIzOCw0MywxNTAsNzYsOTIsMTIzLDAsMjA5LDE1MiwzNywxNywyMTIsMTE0LDExNiw1OSwxNCwxMTQsMCwyMzgsNzAsMTcyLDI0NiwxODcsMTU3LDEzMiw2MywyNDksNjEsMjAxLDg3LDI1MSwxOTMsMjM2LDIyNCwxOTUsMjI2LDEyLDExNSwxOTEsMzAsODEsMjM0LDIyMiwxMjIsMTI1LDYsNCwyMTUsMTg1LDYsMyw5OCwxODYsMTM3LDEzNywxNjUsMjUsMTIyLDg4LDI0NCw2NywxMjksMjEzLDgsMTQ5LDcyLDMsMTA3LDI0LDIxNCwyNywyMTIsMjEyLDc3LDIxNCwyOSwxNTksMjksMjExLDQ4LDcw
Example link for doing Base 64 encoding: https://www.base64encode.org/
I use this encoding as part of the basic authentication header used in the example GET command in Fiddler:
On inspecting the raw output in Fiddler see that the command has satisfied both the authentication and https requirements and the GET command has returned the values as shown:
Also shown is an example console app with which to test the web service:
using System; using System.Net; using System.Text; namespace WebAppConsumer { internal static class Program { private static void Main() { var path = "https://localhost/api/values"; // Update your local service port no. / service APIs etc in the following line var encrypted = Crypto.Encrypt("andy:password"); var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(encrypted)); try { var webClient = new WebClient(); webClient.Headers.Add(HttpRequestHeader.Authorization, "Basic " + encoded); // Sometimes when you get security certificate trouble but you still need to debug //ServicePointManager.ServerCertificateValidationCallback += // (sender, certificate, chain, sslPolicyErrors) => true; var result = webClient.DownloadString(path); Console.WriteLine(result); } catch (Exception e) { Console.WriteLine(e.Message); } Console.ReadLine(); } } }