Search code examples
c#restasp.net-coreasp.net-core-webapiautorest

Web API REST Client - What is the best way to (auto-)authenticate API in Website


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:

  1. \token: it will accept two parameters username and password, and return two things: access_token(JWT Token), and randomly generated refresh_token.

  2. \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.


Solution

  • 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.