{"id":98,"date":"2024-02-15T10:28:17","date_gmt":"2024-02-15T18:28:17","guid":{"rendered":"https:\/\/denispavlov.net\/Blog\/?p=98"},"modified":"2024-03-20T07:09:14","modified_gmt":"2024-03-20T14:09:14","slug":"thread-safe-httpservice-with-jwt-refresh","status":"publish","type":"post","link":"https:\/\/denispavlov.net\/Blog\/2024\/02\/15\/thread-safe-httpservice-with-jwt-refresh\/","title":{"rendered":"Thread Safe HttpService With JWT Refresh"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">One of the most common tasks for backend developers is integrating with third party APIs. And most of the time we end up solving the same problems over and over again, like automatically refreshing the auth token, which eventually expires.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s consider the following scenario. There is a third party API at <code>https:\/\/denispavlov.net<\/code>, which requires authentication. We know that there is a <code>GET \/auth<\/code> endpoint which returns an access token valid for 30 minutes. And we&#8217;re interested in a resource <code>GET \/api<\/code>, which requires that token to be sent in request header as <code>Authorization: Bearer {token}<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To refresh the JWT, I never like to rely on token expiration timestamp coming from a third party server, even when it&#8217;s available, because it&#8217;s not reliable imho (server clocks might be wrong, etc). Instead, I prefer to wait for the first <code>401 Unauthorized<\/code> response to be my indicator that it&#8217;s time for token renewal. Let&#8217;s consider the following code to accomplish this scenario.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System.Net;\n\npublic class HttpService\n{\n    private const string _authUrl = \"https:\/\/denispavlov.net\/auth\";\n    private readonly HttpClient _client;\n    private readonly string _authHeader = HttpRequestHeader.Authorization.ToString();\n    private static string _jwt = string.Empty;\n\n    public HttpService(HttpClient client)\n    {\n        _client = client;\n\n        SetJwt();\n    }\n\n    private void SetJwt()\n    {\n        using var request = new HttpRequestMessage(HttpMethod.Get, _authUrl);\n        using var response = _client.Send(request);\n        using var stream = response.Content.ReadAsStream();\n        using var reader = new StreamReader(stream);\n        _jwt = reader.ReadToEnd();\n    }\n\n    public async Task&lt;string&gt; GetContentAsync(string url)\n    {\n        var response = await SendAsync(HttpMethod.Get, url);\n\n        if (!response.IsSuccessStatusCode)\n        {\n            SetJwt();\n\n            response = await SendAsync(HttpMethod.Get, url);\n        }\n\n        return await response.Content.ReadAsStringAsync();\n    }\n\n    private async Task&lt;HttpResponseMessage&gt; SendAsync(HttpMethod method, string url)\n    {\n        var request = new HttpRequestMessage(method, url);\n        request.Headers.Add(_authHeader, $\"Bearer {_jwt}\");\n        return await _client.SendAsync(request);\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">At the first glance, it looks fine. We inject <code>HttpClient<\/code> using <code>HttpClientFactory<\/code> <a href=\"https:\/\/learn.microsoft.com\/en-us\/aspnet\/core\/fundamentals\/http-requests?view=aspnetcore-8.0#typed-clients\" title=\"Typed Client\">Typed Client<\/a> pattern, we get the access token and store it in a field. When consumer of this class calls GetContentAsync and first response isn&#8217;t successful, refresh access token and retry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This solution will work in a single threaded application where requests come in one at a time. Unfortunately, that is not the reality for most production applications, and this code may have some issues when handling many concurrent requests. The problem is that our token refresh method is not thread safe, therefore you may end up with multiple threads trying to refresh the token simultaneously (potentially making extra unnecessary calls to the third party API, or worse, getting throttled because the <code>GET \/auth<\/code> may have request rate limit). Let&#8217;s consider a better thread-safe solution.<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/MechStar\/b7c7e832da4234be813dc37d21c7cc66.js\"><\/script>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Important Observation #1<\/strong>: The <code>SetJwt<\/code> method has been modified to <code>lock<\/code> the critical section of code to ensures that only one thread can enter at a time. Hence, avoiding potential simultaneous calls to refresh the token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Important Observation #2<\/strong>: The two <code>if<\/code> statements <code>if (string.IsNullOrEmpty(_jwt))<\/code> before and after the lock. This ensures that if there is a race condition where the first thread just executed line 28 and the second thread already entered the first <code>if<\/code> statement on line 21, but hasn&#8217;t entered the <code>lock<\/code>, there is a secondary check to avoid the second renewal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Important Observation #3<\/strong>: The new private <code>ExecuteWithRetriesAsync<\/code> method is a wrapper for all API calls that require access token. It utilizes <code>Polly<\/code> library that is now included in .NET to automatically refresh JWT and retry the failed request once. Line 53 has an important <code>if<\/code> statement to ensure that we&#8217;re clearing out the expired JWT, in case another thread refreshed it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">NOTE: If you anticipate very low traffic on this service, you may move the <code>SetJwt<\/code> call from the constructor to the beginning of <code>GetContentAsync<\/code>, to avoid a situation where the token expired before you got a chance to use it. Make sure any other methods you want to expose that invoke <code>HttpClient<\/code> are wrapped in <code>ExecuteWithRetriesAsync<\/code> as done in my <code>GetContentAsync<\/code> example.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-dots\"\/>\n\n\n","protected":false},"excerpt":{"rendered":"<p>One of the most common tasks for backend developers is integrating with third party APIs. And most of the time we end up solving the same problems over and over again, like automatically refreshing the auth token, which eventually expires. Let&#8217;s consider the following scenario. There is a third party API at https:\/\/denispavlov.net, which requires [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"footnotes":"[]"},"categories":[1],"tags":[3,4,21,22,5,20],"class_list":["post-98","post","type-post","status-publish","format-standard","hentry","category-net","tag-httpclient","tag-httpclientfactory","tag-jwt","tag-lock","tag-polly","tag-thread-safety"],"_links":{"self":[{"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/posts\/98","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/comments?post=98"}],"version-history":[{"count":34,"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/posts\/98\/revisions"}],"predecessor-version":[{"id":158,"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/posts\/98\/revisions\/158"}],"wp:attachment":[{"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/media?parent=98"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/categories?post=98"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/denispavlov.net\/Blog\/wp-json\/wp\/v2\/tags?post=98"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}