A way to restrict APIs that each request can be called with a new key (Time-based One-time Password)
TOTP is an algorithm that uses clocks to generate one-time passwords. In this way, a unique code will be generated at any moment. If you have worked with Google Authenticator, you are familiar with this concept.
In this article, we want to implement a scenario in which APIs must send a unique password with the token. To do this, any user or client who wants to use the API must first log in, and after logging in, we will send him a 16-character ClientSecret with a token. We use ClientSecret to encrypt sent code (TOTP). Like Public / Private key, there is a fixed key on the server side and a fixed key in the mobile application, and when TOTP encryption, in addition to the key in the mobile and server, we use the user's ClientSecret to make the created TOTP completely unique. (We encrypt the TOTP value using the two keys Key and ClientSecret).
In this example, we take the current date (UTC) before calling the API on the mobile and save the ticks to a model. Then we serialize the model that contains the current time and convert it to a string, then we encrypt the obtained string using the AES algorithm using the fixed key and ClientSecret. Then we put this value in the Request-Key header. Note that ClientSecret must also be sent to the server in another header, because using this ClientSecret will provide decryption operations. For example, the current clock in the form of Ticks is "637345971787256752". This number is of long type and its value increases over time. Finally, the final serialized model is as follows:
{"DateTimeUtcTicks":637345812872371593}
And the encrypted value is equal to:
g/ibfD2M3uE1RhEGxt8/jKcmpW2zhU1kKjVRC7CyrHiCHkdaAmLOwziBATFnHyJ3
The submitted model includes a property called DateTimeUtcTicks, which is a long type that creates this model before calling the API and puts the resulting encrypted value in the Request-Key header, along with ClientSecret in the ClientSecret header. This must be done on the mobile side and on the server side must be sent using ClientSecret and the fixed key on the server to decrypt this header. Because this is not very time consuming and finally there is a minute difference in time between the request sent and the time of receipt of the request on the server, on the server side we calculate the value obtained (the value of DateTimeUtcTicks sent decrypted) as the value sent The current time must be greater than or equal to one minute before and less than or equal to the current date. like this
var dateTimeNow = DateTime.UtcNow;
var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
var expireTimeTo = dateTimeNow.Ticks;
string clientSecret = httpContext.Request.Headers["ClientSecret"].ToString();
string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);
if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
Finally, if the value sent is between this interval, it means that the request is valid, otherwise we have to show the relevant error to the user. Next, to implement this scenario, we have used a middleware that first checks whether the submitted request contains the Request-Key and ClientSecret headers or not? If the header is empty or the header value is null, we show error 403 to the user. To prevent re-use of the encrypted header, when the first request is sent to the server, we save the encrypted string in the cache, and if it sends the same string again, we will not allow it to access the API. Hold the cache for 2 minutes; Because we have finally considered a minute difference for each request.
Next, if the encrypted string is available in the cache, we will again display the message "Forbidden: You do not have permission to call this api"; Because it means that the encrypted string has already been sent. Then we decrypt the encrypted string and disserial it to the ApiLimiterDto model and check that the number of Ticks sent by the mobile is more than a minute ago and less than now. If it is between these two intervals, it means that the request is valid and it is allowed to call the API; Otherwise we display the message 403 to the user.
ApiLimiterDto model:
public class ApiLimiterDto
{
public long DateTimeUtcTicks { get; set; }
}
Firmware :
public class ApiLimiterMiddleware
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;
public ApiLimiterMiddleware(RequestDelegate next, IDistributedCache cache)
{
_next = next;
_cache = cache;
}
private const string requestKey = "Request-Key";
private const string clientSecretHeader = "ClientSecret";
public async Task InvokeAsync(HttpContext httpContext)
{
if (!httpContext.Request.Headers.ContainsKey(requestKey) || !httpContext.Request.Headers.ContainsKey(clientSecretHeader))
{
await WriteToReponseAsync();
return;
}
var requestKeyHeader = httpContext.Request.Headers[requestKey].ToString();
string clientSecret = httpContext.Request.Headers[clientSecretHeader].ToString();
if (string.IsNullOrEmpty(requestKeyHeader) || string.IsNullOrEmpty(clientSecret))
{
await WriteToReponseAsync();
return;
}
if (_cache.GetString(requestKeyHeader) != null)
{
await WriteToReponseAsync();
return;
}
var dateTimeNow = DateTime.UtcNow;
var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
var expireTimeTo = dateTimeNow.Ticks;
string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);
if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
{
//ذخیره کلید درخواست در کش برای جلوگیری از استفاده مجدد از کلید
await _cache.SetAsync(requestKeyHeader, Encoding.UTF8.GetBytes("KeyExist"), new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2)
});
await _next(httpContext);
}
else
{
await WriteToReponseAsync();
return;
}
async Task WriteToReponseAsync()
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
await httpContext.Response.WriteAsync("Forbidden: You don't have permission to call this api");
}
}
}
For encryption and decryption, we have created a class called AesProvider that provides encryption and decryption operations.
public static class AesProvider
{
private static byte[] GetIV()
{
return encoding.GetBytes("ThisIsASecretKey");
}
public static string Encrypt(string plainText, string key)
{
try
{
var aes = GetRijndael(key);
ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] buffer = encoding.GetBytes(plainText);
string encryptedText = Convert.ToBase64String(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length));
return encryptedText;
}
catch (Exception)
{
throw new Exception("an error occurred when encrypting");
}
}
private static RijndaelManaged GetRijndael(string key)
{
return new RijndaelManaged
{
KeySize = 128,
BlockSize = 128,
Padding = PaddingMode.PKCS7,
Mode = CipherMode.CBC,
Key = encoding.GetBytes(key),
IV = GetIV()
};
}
private static readonly Encoding encoding = Encoding.UTF8;
public static string Decrypt(string plainText, string key)
{
try
{
var aes = GetRijndael(key);
ICryptoTransform AESDecrypt = aes.CreateDecryptor(aes.Key, aes.IV);
byte[] buffer = Convert.FromBase64String(plainText);
return encoding.GetString(AESDecrypt.TransformFinalBlock(buffer, 0, buffer.Length));
}
catch (Exception)
{
throw new Exception("an error occurred when decrypting");
}
}
}
The Decrypt and Encrypt methods receive an input called a key that is received from the ClientSecret header. On the server side, the Decrypt operation is practically performed, and Encrypt is not used on the server side for this example.
For encryption using the AES method, since we have used 128 bits, the length of the key variable must be 16 characters and IV must be less than or equal to 16 characters.
Finally, to use this middleware, we can use MiddlewareFilter , which we can use for some of the desired applications.
ApiLimiterPipeline class:
public class ApiLimiterPipeline
{
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ApiLimiterMiddleware>();
}
}
How to use the middleware for a specific action:
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
[MiddlewareFilter(typeof(ApiLimiterPipeline))]
public async Task<IActionResult> Get()
{
return Ok("Hi");
}
}
;)
Powered by Froala Editor