I have a .Net Core MVC Website consuming .Net Core Web API Rest service. Rest client code is generated by AutoRest.
For authentication purpose, API has two endpoints:
\token:
it will accept two parameters username and password, and
return two things: access_token
(JWT Token), and randomly generated
refresh_token
.
\token\refresh
which will accept two parameters: access_token
and
refresh_token
and return the new access_token
and new refresh_token
.
access_token
lifetime is 24 hours,
refresh_token
lifetime is 5 days
Lets move to website part. UserController
has 5 standard action methods, Index, Details, Create, Edit and Delete
.
On the very first request to Index route, retrieving list of all users. I am getting token by providing username and password to the API Client generated by AutoRest.
string access_token = GetAccessToken(username, password); // this will call API's \token endpoint and return access_token
HttpContext.Session.SetString("api_access_token", access_token); // put this token in session variable so it can be used for further requests.
var tokenCredentials = new Microsoft.Rest.TokenCredentials(access_token);
var api = new ApiServiceClient2.ApiServiceClientProxy2(BaseUri, tokenCredentials);
Then I can call my actual request to get users list
IList<ApiServiceClient.Models.AppUser> list = api.AppUser.GetAppUser();
Here one request is completed.
Lets move to second request (Details route), I am getting user detail for specific id, here I can retrieve token from session, and remaining part is same, create credentials object and call target action method.
string access_token = HttpContext.Session.GetString("api_access_token");
var tokenCredentials = new Microsoft.Rest.TokenCredentials(access_token);// I can put this token in session variable so it can be used for further requests.
var api = new ApiServiceClient2.ApiServiceClientProxy2(BaseUri, tokenCredentials);
ApiServiceClient.Models.AppUser obj = api.AppUser.GetAppUser1(id);
Similary I can write Create, Edit and Delete
action methods, by getting token from session and passing to the API client.
Now what if my token got expired, how could the website come to know that it has to refresh the token by sending request to \token\refresh
endpoint. Also when refresh token is get expired, then generate new token by re-sending username and password to \token
endpoint.
So what is the best approach to call API with this authentication scheme. Should I write this logic(generate token, check for token expiry, refresh token, another check for refresh token expiry) in each action method of my controller? Obviously a reasonable website do not have only one controller, a website with 10-15 controllers, each has these 5 action methods, it will be very cumbersome to write this same logic in each action method.
As I mentioned I have generated the API client code using AutoRest tool. I want to use these auto-generated model classes and api client. Which makes it little more difficult to where to inject this logic.
One possible way to do it is using ServiceClientCredentials
.
When you configure your RestClient for DI/IoC container (code for it wasn't added last to the generator last time I looked), you can also inject a custom/configured HttpClient
instance into it.
Extending the generated RestClient is rather trivial to support Dependency Injection with custom (and pooled) HttpClient for better performance and resource management (See You're Using HttpClient Wrong.
First you need to add a header to your RestClient that supports injection of HttpClient
.
You create a new file MyRestClient.Customizations.cs
and
public partial class MyRestClient
{
public MyRestClient(IOptions<MyRestClientOptions> options, HttpClient httpClient, AutoRefreshingCredentials credentials)
: base(httpClient, disposeHttpClient: true)
{
// to setup url in Startup.ConfigureServices
BaseUri = options.Value.BaseUri;
Credentials = credentials ?? throw new ArgumentNullException(nameof(credentials));
}
}
note that it's a partial class
. This way we can add custom methods, properties, constructors to the class w/o risk it being overridden the next time the generator runs.
In your ConfigureServices
setup dependency injection and the MyRestClientOptions
.
services.AddScoped<AutoRefreshCredentials>();
services.Config<MyRestClientOptions>(options =>
{
options.BaseUri = new Uri("https://example.com/my/api/");
});
services.AddHttpClient<IMyRestClient, MyRestClient>()
// Retrial policy with Poly
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(4, (t) => TimeSpan.FromSeconds(t)));
Finally add your AutoRefreshCredentials
class
public class AutoRefreshingCredentials : ServiceClientCredentials
{
public const string AuthorizationHeader = "Authorization";
public AutoRefreshingCredentials (HttpClient httpClient)
{
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public HttpClient HttpClient { get; }
public override Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// TODO: Check if token is valid and/or obtain a new one
string token = await GetOrRefreshTokenAsync(...);
request.Headers.Add(AuthorizationHeader, token);
return base.ProcessHttpRequestAsync(request, cancellationToken);
}
}
Then just inject your MyRestClient
client wherever you need it. Be aware of concurrency though which may trigger multiple sign-ups/token refreshing though.