Related article
How to integrate ApiFrame in ASP.NET Web API Application[^]
Contents
Introduction
ApiFrame is a simple .NET library that implements the core components required for ASP.NET WEB API development. It's a C# class library with logical separation of Server (Provider), Client (Consumer) and the supporting Framework (An Infrastructure). The Server components provide an interface to implement security (Authentication & Authorization), exception handling and versioning of Web API methods. The security mechanism implemented in this library is HMAC (Hash Message Authentication Code) authentication. One example of HMAC usage is Amazon web service (AWS) and the authentication process in this library uses a HMAC-SHA signature which is the same approach as AWS. The Client part provides a Gateway component that can be referenced by the .NET clients to consume the WEB API.
This article will serve as a simple documentation for ApiFrame that focus on providing a basic description about the class components within the library. The diagram below shows the outline of component architecture.
HMAC Authentication – An Overview
HMAC authentication provides a simple way to authenticate a HTTP request using a secret key that is known to client and server. Both client and server have access to secret key. Generally, this secret key is a Unique Id which is created at the time of registration and stored in the database. Using the secret key and a message based on request content client generates a signature (MAC) using HMAC algorithm and this signature is attached to the authorization header of HTTP request. When the server receives the request, it extracts the hashed signature (MAC) from the request header and calculates its own version of signature to verify if the received signature matches the calculated signature. If the two signature matches, then the system concludes that the request is valid and should be served. If the two signatures don’t match, then the request is dropped and the system responds with an error message.
Framework
The Framework provides token models, configuration class, constant values, helpers, extensions to support the implementation of server and client components. Also, it offers an Interface to integrate with the other applications through dependency injection.
Tokens
HMAC authentication depends on token based communication. A Token is a small piece of data that is attached to the authorization header of every API request. In general, Tokens are keys that are used for authentication and authorization of the HTTP request. In the below class diagram, you will notice that Base class "ApiBaseToken" has the following properties
- AccessToken: Access token is a Public key
- SecretToken: Secret Token is a private key used for generating the signature using a hash algorithm
- AuthScheme: AuthScheme is a simple abbreviated text that indicates the consumer (application / customer) name
There are three classes derived from this Base class, each represents a token model used for a distinct purpose. ApiApplicationToken is required for Authentication. ApiUserToken is required for Authorization. ApiRequestToken is required for creating a client request.
Interface
There are few areas in the library where dependencies need to be injected. To achieve this, the required interfaces are defined. The using application should supply the required tokens to authenticate and authorize an HTTP request. The interface IApiInception expose the necessary methods that the calling assembly need to implement which returns the tokens required to process the HTTP Request. The interface IApiException is for exception logging. Every application has its own exception logging mechanism. When an unhandled program exception is thrown in the using application, It can be caught by implementing the IApiException interface. The interface IApiSignature is to calculate HMAC signature. The library itself contains an implementation of this interface that uses HMACSHA256 algorithm to calculate the HMAC signature. The same implementation can be used or a different implementation can be injected using this interface if required.
public interface IApiInception
{
ApiApplicationToken GetApplicationToken(string accessToken);
ApiUserToken GetUserToken(string username, string password);
ApiUserToken GetUserToken(string accessToken);
}
public interface IApiException
{
void LogMessage(Exception exception);
}
public interface IApiSignature
{
string CalculateHmac(string secretKey, string stringToSign);
}
Dependency Injection
A simple Dependency Injection class is defined with two generic static methods that allows to inject objects into a class. RegisterType<TInterface, TClass> method is called by the using application to map an interface type with corresponding concrete class type. GetInstance<TInterface> method is called by the internal class components to get the object instance of the registered interface type.
public class ApiObjectFactory
{
private static readonly Dictionary<string, System.Type> ObjectTypes
= new Dictionary<string, System.Type>();
public static void RegisterType<TInterface, TClass>() where TClass : TInterface
{
var typeInterface = typeof(TInterface);
var typeClass = typeof(TClass);
if (!typeInterface.IsInterface || typeClass.IsInterface || typeClass.IsAbstract)
{
throw new ApiRequestException(ApiErrorCode.InvalidOperation);
}
ObjectTypes.Add(typeInterface.Name, typeClass);
}
public static TInterface GetInstance<TInterface>()
{
System.Type type = null;
if (ObjectTypes.TryGetValue(typeof(TInterface).Name, out type))
{
return (TInterface)System.Activator.CreateInstance(type);
}
throw new ApiRequestException(ApiErrorCode.MissingRequiredType);
}
}
Signature Calculation
When implementing HMAC authentication, Every API request requires signing with HMAC signature. The signature is computed with the token “SecretKey” and a message “StringToSign”. The “StringToSign” is constructed using the URI, request timestamp and other HTTP header values. The computed signature is converted to base64 string. This encoded base64 string is the calculated signature used for signing the HTTP request.
public string CalculateHmac(string secretKey, string stringToSign)
{
byte[] secretBytes = Encoding.UTF8.GetBytes(secretKey);
byte[] stringBytes = Encoding.UTF8.GetBytes(stringToSign);
string signature;
using (var hmac = new HMACSHA256(secretBytes))
{
byte[] hash = hmac.ComputeHash(stringBytes);
signature = Convert.ToBase64String(hash);
}
return signature;
}
Configuration
A Configuration class "ApiConfiguration" implements a singleton instance with properties shown in the below class diagram. "RequestValidityInMinutes" property is used to configure the request validity in minutes, that no HTTP Request can be older than x minutes. The x value is configured through the instance of ApiConfiguration class. Its initial value defaults to 10 minutes. This can be modified by the using application. "VersionNamespaceKey" property is used to configure the Web API versioning method. ApiFrame allows two different method to implement versioning. It can be configured using "VersionNamespaceKey" property and it defaults to "area"
public class ApiConfiguration
{
static ApiConfiguration()
{
Instance = new ApiConfiguration();
Instance.RequestValidityInMinutes = 10;
Instance.VersionNamespaceKey = "area";
}
public static ApiConfiguration Instance { get; private set; }
public double RequestValidityInMinutes { get; set; }
public string VersionNamespaceKey { get; set; }
}
Utilities
This part of the library includes utility classes such as helpers, extensions and validation methods to access and validate the HTTP request and headers.
Server
The server components implements the required filters for Authentication and Authorization. The filters verifies the HTTP request and identifies the requester. Verifying a HTTP request on the server side involves three steps which is shown in the below diagram. The first step is to retrieve the secret token using the access token. The second step is to calculate the signature from the request parameters and secret token. The third step is to verify if the calculated signature matches the received signature.
Security
The class diagram below shows the design of the server component that handles the Authentication and Authorization.
Authentication
Authentication is identifying the user based on username and password. ApiFrame implements an authorization filter attribute “ApiAuthentication” that follows the HMAC method such as validating the signature to authenticate a user. It reads the username and password from HTTP request header and uses it to identify the user. If the user is valid, then it creates a custom identity and principal object. This custom principal object is set to the user property of current HttpContext.
public class ApiAuthentication : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
HttpRequestMessage request = actionContext.Request;
IApiValidation apiValidation = new ApiValidation();
apiValidation.ValidateRequest(request);
string apiAccessToken = request.GetAccessToken();
IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();
ApiApplicationToken applicationToken = apiInception.GetApplicationToken(apiAccessToken);
apiValidation.ValidateToken(request, applicationToken);
apiValidation.ValidateSignature(request, applicationToken.SecretToken);
var username = HttpContext.Current.Request.Params["Username"];
var password = HttpContext.Current.Request.Params["Password"];
if (username == null || password == null)
{
throw new ApiRequestException(ApiErrorCode.MissingRequiredParamter);
}
ApiUserToken user = apiInception.GetUserToken(username, password);
bool isAuthenticated = user != null;
if (isAuthenticated)
{
string authenticationType = ApiConstants.AUTHTYPE;
IIdentity userIdentity = new ApiIdentity(authenticationType, isAuthenticated, user.Name, user.UserId);
IPrincipal principal = new ApiPrincipal(userIdentity, user.Roles);
HttpContext.Current.User = principal;
}
else
{
throw new ApiRequestException(ApiErrorCode.AuthenticationFailed);
}
}
}
Authorization
Authorization is for checking whether the authenticated user is allowed to perform an action or access a protected resource. In ApiFrame, for authorization we have an authorization attribute class named “ApiAuthorization” derived from “AuthorizeAttribute” that validates the signature and identifies the authenticated user using the User Token in the HTTP request.
public class ApiAuthorization : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
HttpRequestMessage request = actionContext.Request;
IApiValidation apiValidation = new ApiValidation();
apiValidation.ValidateRequest(request);
string accessToken = request.GetAccessToken();
IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();
ApiUserToken user = apiInception.GetUserToken(accessToken);
apiValidation.ValidateToken(request, user);
apiValidation.ValidateSignature(request, user.SecretToken);
if (!string.IsNullOrEmpty(Roles))
{
apiValidation.ValidateRole(Roles, user.Roles);
}
string authenticationType = ApiConstants.AUTHTYPE;
IIdentity userIdentity = new ApiIdentity(authenticationType, true, user.Name, user.UserId);
IPrincipal principal = new ApiPrincipal(userIdentity, user.Roles);
HttpContext.Current.User = principal;
}
}
Authorized Request
If the using application doesn't require authentication and authorization for public API methods but want to allow access to athorized clients then In that case, an authorization filter attribute "ApiAuthorizedRequest" is defined. This can be applied to action methods to checks if the request is from an authorized client.
public class ApiAuthorizedRequest : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
HttpRequestMessage request = actionContext.Request;
IApiValidation apiValidation = new ApiValidation();
apiValidation.ValidateRequest(request);
string apiAccessToken = request.GetAccessToken();
IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();
ApiApplicationToken applicationToken = apiInception.GetApplicationToken(apiAccessToken);
apiValidation.ValidateToken(request, applicationToken);
apiValidation.ValidateSignature(request, applicationToken.SecretToken);
}
}
Custom Identity and Principal
When the server authenticates / authorizes the user, it creates a custom principal (ApiPrincipal) which is an IPrincipal object that represents the security context under which code is running. The principal contains an associated custom identity object (ApiIdentity) that contains information about the user. This security information (ApiPrincipal) is set for the current HTTP request (HttpContext.Current.User) when authenticated / authorized. The below code shows the custom Identity and Principal class
public class ApiIdentity : IIdentity
{
public ApiIdentity(string authenticationType, bool isAuthenticated, string userName, string userId)
{
this.AuthenticationType = authenticationType;
this.IsAuthenticated = isAuthenticated;
this.Name = userName;
this.UserId = userId;
}
public string AuthenticationType { get; private set; }
public bool IsAuthenticated { get; private set; }
public string Name { get; private set; }
public string UserId { get; private set; }
}
public class ApiPrincipal : IPrincipal
{
public ApiPrincipal(IIdentity identity, string roles)
{
this.Identity = identity;
this.Roles = roles.Split(',');
}
public IIdentity Identity { get; private set; }
public string[] Roles { get; private set; }
public bool IsInRole(string roles)
{
return Roles.Intersect(roles.Split(',')).Count() > 0;
}
}
Enforcing HTTPS
The communication through plain HTTP is not secure though we have secure authentication schemes. Enabling SSL is another layer of protection to data. We may need HTTPS for access to some protected resources. ApiFrame implements an authorization filter named “ApiHttpsRequired” that checks for SSL. This can be used for Web API methods that require HTTPS.
public class ApiHttpsRequired : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
throw new ApiRequestException(ApiErrorCode.InvalidUriScheme);
}
else
{
base.OnAuthorization(actionContext);
}
}
}
Exception
Error Code
A set of error codes are defined in a enum type called ApiErrorCode that contains the list of error types handled by the ApiFrame.
Error Response
Error response are created by ApiErrorResponse class that contains a static method "GetErrorMessage" which returns a HttpResponseMessage for a given ApiErrorCode. The method identifies the custom error with the error code and creates a HttpError with the custom message, type and appropriate HttpStatusCode. This HttpError is serialized and assigned to the content of HttpResponseMesssage. ApiErrorResponse class provides another method "GetHttpError" to deserialize the content of HttpResponseMessage to HttpError.
public class ApiErrorResponse
{
private const string Code = "Code";
private const string Type = "Type";
public static HttpResponseMessage GetErrorMessage(ApiErrorCode errorCode)
{
HttpError error;
switch (errorCode)
{
case ApiErrorCode.InvalidRequestHeader:
case ApiErrorCode.InvalidMD5:
case ApiErrorCode.InvalidSignature:
error = new HttpError("Problem communicating with the application. Invalid Request.");
error[Code] = HttpStatusCode.ExpectationFailed;
break;
case ApiErrorCode.InvalidTimestamp:
error = new HttpError("The date and time is incorrect.");
error[Code] = HttpStatusCode.Forbidden;
break;
case ApiErrorCode.InvalidScheme:
error = new HttpError("This version of application is outdated.");
error[Code] = HttpStatusCode.BadRequest;
break;
case ApiErrorCode.InvalidUriScheme:
error = new HttpError("There has been problem processing your request.");
error[Code] = HttpStatusCode.Forbidden;
break;
case ApiErrorCode.AuthenticationFailed:
error = new HttpError("The username/passowrd you have entered is incorrect");
error[Code] = HttpStatusCode.Forbidden;
break;
case ApiErrorCode.InvalidToken:
case ApiErrorCode.InvalidRole:
error = new HttpError("Authorization has been denied for this request");
error[Code] = HttpStatusCode.Unauthorized;
break;
case ApiErrorCode.MissingRequiredParamter:
error = new HttpError("The username/passowrd parameter is missing");
error[Code] = HttpStatusCode.Forbidden;
break;
case ApiErrorCode.MissingRequiredType:
error = new HttpError("The required type is not registered");
error[Code] = HttpStatusCode.Forbidden;
break;
default:
error = new HttpError("Server error.");
error[Code] = HttpStatusCode.InternalServerError;
break;
}
error[Type] = errorCode.ToString();
var response = new HttpResponseMessage((HttpStatusCode)error[Code])
{
Content = new StringContent(new JavaScriptSerializer().Serialize(error)),
ReasonPhrase = errorCode.ToString()
};
return response;
}
public static HttpError GetHttpError(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
string responseError = response.Content.ReadAsStringAsync().Result;
var httpError = new JavaScriptSerializer().Deserialize<HttpError>(responseError);
return httpError;
}
return null;
}
}
Custom Http Request Exception
A class "ApiRequestException" is derived from HttpResponseException that provides a way to define the custom exception. The ApiRequestException class offers two constructors. One is to define the internal exceptions of ApiFrame and the other constructor allows the calling assembly to initialize its own custom exception.
public class ApiRequestException : HttpRequestException
{
public ApiRequestException(ApiErrorCode errorType)
{
this.ErrorType = errorType;
this.ErrorResponseMessage = ApiErrorResponse.GetErrorMessage(errorType);
}
public ApiRequestException(HttpStatusCode statusCode, string errorMessage, string reasonPhrase)
{
this.ErrorType = ApiErrorCode.InvalidOperation;
this.ErrorResponseMessage = new HttpResponseMessage(statusCode)
{
Content = new StringContent(errorMessage),
ReasonPhrase = reasonPhrase
};
}
public ApiErrorCode ErrorType { get; set; }
public HttpResponseMessage ErrorResponseMessage { get; set; }
}
Exception Filter attribute
Web API exceptions can be handled by an exception filter. ApiExceptionAttribute is an exception filter derived from ExceptionFilterAttribute class and overrides the OnException method.
public class ApiExceptionAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
HttpResponseMessage response;
if (context.Exception is ApiRequestException)
{
ApiRequestException apiException = (ApiRequestException)context.Exception;
response = apiException.ErrorResponseMessage;
}
else
{
ApiObjectFactory.GetInstance<IApiException>().
LogMessage(context.Exception);
response = ApiErrorResponse.GetErrorMessage(ApiErrorCode.InvalidProgramException);
}
context.Response = response;
}
}
Versioning
A custom class “ApiControllerSelector” is defined that implement IHttpControllerSelector to support versioning. ApiFrame provides an option to version Web APIs using Namespaces or Areas. The code is taken from this MSDN blog[^] and is slightly tweaked to work with MVC Areas.
Controller Selector
The interface that Web API uses to select a controller is IHttpControllerSelector. The method on this interface is SelectController, which selects a controller for an HttpRequestMessage. A custom class “ApiControllerSelector” is defined that implement IHttpControllerSelector to support versioning.
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string namespaceName = GetRouteVariable<string>(routeData, ApiConfiguration.Instance.VersionNamespaceKey);
if (namespaceName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string controllerName = GetRouteVariable<string>(routeData, ControllerKey);
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string key = ApiConfiguration.Instance.VersionNamespaceKey == NamespaceKey ?
String.Format(CultureInfo.InvariantCulture, "{0}.Controllers.{1}", namespaceName, controllerName) :
String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);
HttpControllerDescriptor controllerDescriptor;
if (controllers.Value.TryGetValue(key, out controllerDescriptor))
{
return controllerDescriptor;
}
else if (duplicates.Contains(key))
{
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this request."));
}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
Client
The client part implements a Gateway component which can be used by API clients to consume the WEB API methods. Below shown is the class diagram of the Client. The ApiRequestToken contains the required properties to create an Http request which is passed as an input parameter to the execute method of Gateway class component. The primary job of the Gateway is to send the HTTP request to the server and receive the response.
Gateway
Gateway component is responsible for creating a signed HTTP request and sending it to server.Sending a signed HTTP request involves three steps as shown in the below diagram. The first step is to construct a HTTP request with all the required request header parameters. The second step is to create a HMAC-SHA signature using the secret token and “StringToSign” message based on request content. The third step is to send the request and the signature to the server.
private HttpResponseMessage SendHttpRequest(ApiRequestToken requestToken)
{
DateTime requestDate = ApiHelper.GetCurrentDateTime();
string contentType = string.Empty;
string contentMD5 = string.Empty;
if (!string.IsNullOrEmpty(requestToken.Content))
{
contentType = ApiConstants.CONTENTTYPE;
contentMD5 = ApiHelper.ComputeMD5Hash(requestToken.Content);
}
HttpRequestMessage request = new HttpRequestMessage(requestToken.Verb, requestToken.RelativeUrl);
request.Headers.Date = requestDate;
if (!string.IsNullOrEmpty(requestToken.Content))
{
request.Content = new StringContent(requestToken.Content);
request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
request.Content.Headers.Add("Content-MD5", contentMD5);
}
string stringToSign = ApiHelper.BuildMessageRepresentation(requestToken.Verb.ToString(), contentType, contentMD5, requestDate, ApiConstants.FORWARDSLASH + requestToken.RelativeUrl);
string signature = this.signature.CalculateHmac(requestToken.SecretToken, stringToSign);
string authorizationHeader = ApiHelper.BuildAuthorizationHeader(requestToken.AuthScheme, requestToken.AccessToken, signature);
request.Headers.Add("Authorization", authorizationHeader);
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(this.baseUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApiConstants.APPJSON));
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
HttpResponseMessage response = client.SendAsync(request).Result;
return response;
}
}
“Message” based on request content (StringToSign)
StringToSign is a message that is constructed using the elements of a HTTP request. Following is the sample code that illustrates how the StringToSign is constructed. Content-MD5 and Content-Type will be available only for HTTP POST request. For other http request such as GET, PUT, DELETE, the Content value is simply represented as empty.
StringToSign = HTTP-VERB + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
TimeStamp + "\n" +
RequestUri;
Signing the Request
Signing the request is adding the HMAC signature to the authorization header of the HTTP request in the below given form:
Authorization: AuthScheme AccessToken:Signature
Conclusion
The article described the parts of the library and what it contains. A related article posted on How to integrate ApiFrame in ASP.NET Web API Application[^] that provides the guidelines to use APIFrame.
References