Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
HMAC algorithm implemented in C# as an ActionFilterAttribute
/// <summary>
/// - both client and server have access to a api key and consumer secret key that will be used to generate HMAC
/// – the consumer secret its a generated password
/// - using the secret client generates a message signature using HMAC algorithm (the algorithm is provided by .NET framework),
/// - signature is attached to the message (eg. as a header) and the message is sent,
/// - the server receives the message and calculates its own version of the signature using the secret (both client and server use the same HMAC algorithm),
/// - if the signature computed by the server matches the on the message it means that the message is authenticated. ta-dam!
/// </summary>
public class ApiHmacAuthenticationAttribute : ActionFilterAttribute
{
// Authentication parameter
private const string AuthenticationHeaderName = "Authentication";
// Will prevent replay attacks
private const string TimestampHeaderName = "Timestamp";
public override void OnActionExecuting(HttpActionContext actionContext)
{
bool isAuthenticated = IsAuthenticated(actionContext);
if (!isAuthenticated)
{
var response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
actionContext.Response = response;
}
}
#region private methods
/// <summary>
/// This method is used to generate the request signatures
/// </summary>
/// <param name="hashedPassword"></param>
/// <param name="message"></param>
/// <returns></returns>
private static string ComputeHash(string hashedPassword, string message)
{
var key = Encoding.UTF8.GetBytes(hashedPassword);
string hashString;
using (var hmac = new HMACSHA256(key))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
hashString = Convert.ToBase64String(hash);
}
return hashString;
}
private static void AddNameValuesToCollection(List<KeyValuePair<string, string>> parameterCollection, NameValueCollection nameValueCollection)
{
if (!nameValueCollection.AllKeys.Any())
return;
foreach (var key in nameValueCollection.AllKeys)
{
var value = nameValueCollection[key];
var pair = new KeyValuePair<string, string>(key, value);
parameterCollection.Add(pair);
}
}
private static List<KeyValuePair<string, string>> BuildParameterCollection(HttpActionContext actionContext)
{
// Use the list of keyvalue pair in order to allow the same key instead of dictionary
var parameterCollection = new List<KeyValuePair<string, string>>();
var queryStringCollection = actionContext.Request.RequestUri.ParseQueryString();
var formCollection = HttpContext.Current.Request.Form;
AddNameValuesToCollection(parameterCollection, queryStringCollection);
AddNameValuesToCollection(parameterCollection, formCollection);
return parameterCollection.OrderBy(pair => pair.Key).ToList();
}
private static string BuildParameterMessage(HttpActionContext actionContext)
{
var parameterCollection = BuildParameterCollection(actionContext);
if (!parameterCollection.Any())
return string.Empty;
var keyValueStrings = parameterCollection.Select(pair =>
string.Format("{0}={1}", pair.Key, pair.Value));
return string.Join("&", keyValueStrings);
}
private static string GetHttpRequestHeader(HttpHeaders headers, string headerName)
{
if (!headers.Contains(headerName))
return string.Empty;
return headers.GetValues(headerName)
.SingleOrDefault();
}
/// <summary>
/// Builds the message
/// Message eg: GET\nThursday, May 21, 2014 05:23:12 PM\n/api/orders\nkey1=value1&key2=value2&key3=value3
/// </summary>
/// <param name="actionContext"></param>
/// <returns></returns>
private static string BuildBaseString(HttpActionContext actionContext)
{
var headers = actionContext.Request.Headers;
string date = GetHttpRequestHeader(headers, TimestampHeaderName);
string methodType = actionContext.Request.Method.Method;
var absolutePath = actionContext.Request.RequestUri.AbsolutePath.ToLower();
var uri = HttpContext.Current.Server.UrlDecode(absolutePath);
string parameterMessage = BuildParameterMessage(actionContext);
// Message could (in this case should) follow this pattern.
string message = string.Join("\n", methodType, date, uri, parameterMessage);
return message;
}
/// <summary>
/// Verifies if the signature and the hash are the same.
/// </summary>
/// <param name="hashedPassword"></param>
/// <param name="message"></param>
/// <param name="signature"></param>
/// <returns></returns>
private static bool IsAuthenticated(string hashedPassword, string message, string signature)
{
if (string.IsNullOrEmpty(hashedPassword))
return false;
var verifiedHash = ComputeHash(hashedPassword, message);
if (signature != null && signature.Equals(verifiedHash))
return true;
return false;
}
/// <summary>
/// Checks the timestamp to prevent timming attacks.
/// </summary>
/// <param name="timestampString"></param>
/// <returns></returns>
private static bool IsDateValidated(string timestampString)
{
DateTime timestamp;
bool isDateTime = DateTime.TryParse(timestampString, CultureInfo.GetCultureInfo("nl-NL"), DateTimeStyles.AdjustToUniversal, out timestamp);
if (!isDateTime)
return false;
var now = DateTime.UtcNow;
// Todo : Change it to 30 seconds ?
// TimeStamp should not be in 1 minutes behind
if (timestamp < now.AddMinutes(-1))
return false;
if (timestamp > now.AddMinutes(1))
return false;
return true;
}
/// <summary>
/// Every signature is unique, so always added to the memory cache if it's valid.
/// </summary>
/// <param name="signature"></param>
/// <returns></returns>
private static bool IsSignatureValidated(string signature)
{
var memoryCache = MemoryCache.Default;
if (memoryCache.Contains(signature))
return false;
return true;
}
/// <summary>
/// If signature not in the memory cache, adds it.
/// </summary>
/// <param name="signature"></param>
private static void AddToMemoryCache(string signature)
{
var memoryCache = MemoryCache.Default;
if (!memoryCache.Contains(signature))
{
var expiration = DateTimeOffset.UtcNow.AddMinutes(5);
memoryCache.Add(signature, signature, expiration);
}
}
/// <summary>
/// Gets the consumer secret from this client.
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
private string GetHashedPassword(string username)
{
var companyEditService = RegisterTypesAPI.Resolve<ICompanyEditService>();
var secretConsumerKey = companyEditService.GetCompanySecretConsumerKey(username);
return !string.IsNullOrEmpty(secretConsumerKey) ? secretConsumerKey : string.Empty;
}
private bool IsAuthenticated(HttpActionContext actionContext)
{
var headers = actionContext.Request.Headers;
// FileLogger.LogMessage(GetHttpRequestHeader(headers, TimestampHeaderName));
// FileLogger.LogMessage(GetHttpRequestHeader(headers, "Authentication"));
// FileLogger.LogMessage(GetHttpRequestHeader(headers, "Content-type"));
var timeStampString = GetHttpRequestHeader(headers, TimestampHeaderName);
if (!IsDateValidated(timeStampString))
return false;
var authenticationString = GetHttpRequestHeader(headers, AuthenticationHeaderName);
if (string.IsNullOrEmpty(authenticationString))
return false;
var authenticationParts = authenticationString.Split(new[] { ":" },
StringSplitOptions.RemoveEmptyEntries);
if (authenticationParts.Length != 2)
return false;
var username = authenticationParts[0];
var signature = authenticationParts[1];
if (!IsSignatureValidated(signature))
return false;
AddToMemoryCache(signature);
var hashedPassword = GetHashedPassword(username);
var baseString = BuildBaseString(actionContext);
return IsAuthenticated(hashedPassword, baseString, signature);
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment