1

In one of my apps, I've integration with Infusionsoft and access token expires after certain time.

Now the front-end makes multiple requests to get different data. When the token expires, it refreshes the token and gets new access and refresh token. But before I get new access and refresh token, subsequent requests from UI try to refresh the token with old refresh token and they all result in error.

What is the best way to overcome this issue?

Ashit Vora
  • 2,902
  • 2
  • 27
  • 39

2 Answers2

0

(My answer is not specific to Infusionsoft).

Here's an approach I use to prevent duplicate concurrent requests to renew/refresh a bearer-token when a web-service client may make concurrent requests that use the same bearer-token (to prevent each request thread or async-context from making its own separate refresh-request).

The trick is to use a cached Task<AuthResponseDto> (where AuthResponseDto is the DTO type containing the latest successfully obtained access_token) that's swapped inside a lock (you can't await inside a lock, but you can copy Task references inside a lock and then await outside of the lock).

// NOTE: `ConfigureAwait(false)` calls omitted for brevity. You should re-add them back.

class MyHttpClientWrapper
{
    private readonly String refreshTokenOrClientCredentialsOrWhatever;

    private readonly IHttpClientFactory hcf;

    private readonly Object lastAuthTaskLock = new Object();

    private Task<AuthResponseDto> lastAuthTask;
    private DateTime lastAuthTaskAt;

    public MyHttpClientWrapper( IHttpClientFactory hcf )
    {
        this.hcf = hcf ?? throw new ArgumentNullException( nameof(hcf) );

        this.refreshTokenOrClientCredentialsOrWhatever = LoadFromSavedConfig();
    }

    private async Task<AuthResponseDto> RefreshBearerTokenAsync()
    {
        using( HttpClient hc = this.hcf.CreateClient() )
        using( HttpResponseMessage resp = await hc.PostAsync( this.refreshTokenOrClientCredentialsOrWhatever ) )
        {
            AuthResponseDto ar = await DeserializeJsonResponseAsync( resp );
            this.lastAuthTaskExpiresAt = DateTime.UtcNow.Add( ar.MaxAge );
            return ar;
        }
    }

    private async Task<String> RefreshBearerTokenIfNecessaryAsync()
    {
        Task<AuthResponseDto> task;

        lock( this.lastAuthTaskLock )
        {
            if( this.lastAuthTask is null )
            {
                // e.g. This is the first ever request.

                task = this.lastAuthTask = this.RefreshBearerTokenAsync();
            }
            else
            {
                task = this.lastAuthTask;
                
                // Is the task currently active? If it's currently busy then just await it (thus preventing duplicate requests!)
                if( task.IsCompleted )
                {
                    // If the current bearer-token is definitely expired, then replace it:
                    if( this.lastAuthTaskExpiresAt <= DateTime.UtcNow )
                    {
                        task = this.lastAuthTask = this.RefreshBearerTokenAsync();
                    }
                }
                else
                {
                    // Continue below.
                }
            }
        }

        AuthResponseDto ar = await task;
        return ar.BearerToken;
    }

    //

    public async Task<CustomerDto> GetCustomerAsync( Int32 customerId )
    {
        // Always do this in every request to ensure you have the latest bearerToken:
        String bearerToken = await this.RefreshBearerTokenIfNecessaryAsync();
        
        using( HttpClient hc = this.hcf.Create() )
        using( HttpRequestMessage req = new HttpRequestMessage() )
        {
            req.Headers.Add( "Authorization", "Bearer " + bearerToken );
            using( HttpResponseMessage resp = await hc.SendAsync( req ) )
            {
                if( resp.StatusCode == 401 )
                {
                    // Authentication error before the token expired - invoke and await `RefreshBearerTokenAsync` (rather than `RefreshBearerTokenIfNecessaryAsync`) and see what happens. If it succeeds then re-run `req`) otherwise throw/fail because that's an unrecoverable error.
                }

                // etc
            }
        }
    }
}
Dai
  • 141,631
  • 28
  • 261
  • 374
-1

There is a tutorial that talks about refreshing the token on a schedule.

https://developer.infusionsoft.com/tutorials/making-oauth-requests-without-user-authorization/#as-you-go-refresh-the-access-token

  • Hi Michael, let me ask you how reliable 24 hrs schedule, cause it's been around 8 hrs some time ago? – Y. E. Sep 21 '16 at 13:06
  • 2
    Refreshing token on a schedule looks like a weak solution to me, what's going on when your server needs to handle thousands or millions of accounts - talking about 3-legged tokens that requires user credentials? You will just keep refreshing those tokens for each user even if they logged to your app once in a while... sounds like an overkill! – Felipe Nov 02 '16 at 21:58
  • I downvoted this because it's bad advice. In distributed computing - and especially distributed authentication (e.g. OIDC, OAuth2, etc) you can't trust - and shouldn't trust - anything - including when a `refresh_token` or bearer-token is due to expire (because it could be unilaterally revoked before it expires). This is also a real problem when dealing with "reference token"-style `access_token` values which are just short opaque strings like a GUID that don't expose any expiry date - you won't know the `access_token` doesn't work until you get an error back from the server. – Dai Sep 03 '20 at 05:29