Using VS 2017 .Net 4.5.2
I have the following class
public static class MyHttpClient
{
//fields
private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
{
var client = new HttpClient();
await InitClient(client).ConfigureAwait(false);
return client;
});
//properties
public static Task<HttpClient> ClientTask => _Client.Value;
//methods
private static async Task InitClient(HttpClient client)
{
//resey headers
client.DefaultRequestHeaders.Clear();
//Set base URL, NOT thread safe, which is why this method is only accessed via lazy initialization
client.BaseAddress = new Uri(ConfigurationManager.AppSettings["baseAddress"]);//TODO: get from web.config? File? DB?
//create new request to obtain auth token
var request = new HttpRequestMessage(HttpMethod.Post, "/ouath2/token"); //TODO: get from web.config? File? DB? prob consts
//Encode secret and ID
var byteArray = new UTF8Encoding().GetBytes($"{ConfigurationManager.AppSettings["ClientId"]}:{ConfigurationManager.AppSettings["ClientSecret"]}");
//Form data
var formData = new List<KeyValuePair<string, string>>();
formData.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
formData.Add(new KeyValuePair<string, string>("refresh_token", ConfigurationManager.AppSettings["RefreshToken"]));
//set content and headers
request.Content = new FormUrlEncodedContent(formData);
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
//make request
var result = await HttpPost(request, client).ConfigureAwait(false);
//set bearer token
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (string)result.access_token);
//TODO: error handle
}
private static async Task<dynamic> HttpPost(HttpRequestMessage formData, HttpClient client)
{
using (var response = await client.SendAsync(formData).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();//TODO: handle this
return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
}
}
}
Still in progress but I've hit a snag.
This works fine if the token only needs to be fetched once in an applications life, however the API I'm talking to uses short lived bearer tokens (15mins).
As I'm using HttpClient as a static to be reused, I cannot change the Default request headers as they are not threadsafe. But I'm required to request a Bearer token every 15mins.
How would I achieve obtaining a new bearer token and setting the default header in this particular scenario?
update: added the SemaphoreSlim to "lock" the refresh transaction
disclaimer: not tested, might need some tweaking
note 1: the semaphore needs to be in a try/catch/finaly block to ensure release if an error is thrown.
note 2: this version will queue the refresh token calls which significant degrades performance if load is high. To fix this; use a bool indicator to check if the refresh is occurred. This might be a static bool for example
The goal is to only use the refresh token if needed. A fixed interval won't help you because, one day, this interval might change. The correct way to handle this is to retry if a 403 occurs.
You can use a HttpClientHandler to work with your HttpClient.
Override the SendAsync, to handle and retry the 403.
For this to work you'll need this constructor of httpclient:
From the top of my (semi) head, it must be something like this:
the HttpMessageHandler
public class MyHttpMessageHandler : HttpMessageHandler
{
private static SemaphoreSlim sem = new SemaphoreSlim(1);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
//test for 403 and actual bearer token in initial request
if (response.StatusCode == HttpStatusCode.Unauthorized &&
request.Headers.Where(c => c.Key == "Authorization")
.Select(c => c.Value)
.Any(c => c.Any(p => p.StartsWith("Bearer"))))
{
//going to request refresh token: enter or start wait
await sem.WaitAsync();
//some typical stuff
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", yourRefreshToken),
new KeyValuePair<string, string>("client_id", yourApplicationId),
};
//retry do to token request
using ( var refreshResponse = await base.SendAsync(
new HttpRequestMessage(HttpMethod.Post,
new Uri(new Uri(Host), "Token"))
{
Content = new FormUrlEncodedContent(pairs)
}, cancellationToken))
{
var rawResponse = await refreshResponse.Content.ReadAsStringAsync();
var x = JsonConvert.DeserializeObject<RefreshToken>(rawResponse);
//new tokens here!
//x.access_token;
//x.refresh_token;
//to be sure
request.Headers.Remove("Authorization");
request.Headers.Add("Authorization", "Bearer " + x.access_token);
//headers are set, so release:
sem.Release();
//retry actual request with new tokens
response = await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
}
send example, with SendAsync (could also be GetAsync) etc.
public async Task<int> RegisterAsync(Model model)
{
var response = await YourHttpClient
.SendAsync(new HttpRequestMessage(HttpMethod.Post, new Uri(BaseUri, "api/Foo/Faa"))
{
Content = new StringContent(
JsonConvert.SerializeObject(model),
Encoding.UTF8, "application/json")
});
var result = await response.Content.ReadAsStringAsync();
return 0;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With