Search code examples
access-tokenidentityserver4refresh-token

IdentityServer4 Access token updating


Last week I am trying to configure the IdentityServer4 to get an access token automatically updating.

I had an API:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "http://localhost:5100";
                options.RequireHttpsMetadata = false;
                options.ApiName = "api1";  
            });

My MVC client configuration:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";

                options.Authority = "http://localhost:5100";
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code id_token";

                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;

                options.Scope.Add("api1");
                options.Scope.Add("offline_access");
            });

And the IdentityServer's clients configuration:

return new List<Client>
        {
            new Client
            {
                ClientId = "mvc",
                ClientName = "My mvc",
                AllowedGrantTypes = GrantTypes.Hybrid,

                RequireConsent = false,
                AccessTokenLifetime = 10,
                
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },

                RedirectUris           = { "http://localhost:5102/signin-oidc" },
                PostLogoutRedirectUris = { "http://localhost:5102/signout-callback-oidc" },

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "api1"
                },
                AllowOfflineAccess = true
            }

        };

On the client side I use AJAX queries to call the API to get/post/put/delete data. I add the access token to the request and get the result.

private async getAuthenticationHeader(): Promise<any> {
    return axios.get('/token').then((response: any) => {
        return { headers: { Authorization: `Bearer ${response.data}` } };
    });
}

async getAsync<T>(url: string): Promise<T> {
    return this.httpClient
        .get(url, await this.getAuthenticationHeader())
        .then((response: any) => response.data as T)
        .catch((err: Error) => {
            console.error(err);
            throw err;
        });
}

The access token is provided by the MVC client method:

[HttpGet("token")]
public async Task<string> GetAccessTokenAsync()
{
    return await HttpContext.GetTokenAsync("access_token");
}

It works fine. After access token expired I get 401 on the client side, so it would be great to have an opportunity to update access token automatically when it was expired.

According to a documentation I supposed, that It can be reached by setting AllowOfflineAccess to true and adding suitable scope "offline_access".

Maybe I don't understand the right flow of the access and refresh tokens usages. Can I do it automatically or it is impossible? I suppose, that we can use refresh tokens in out queries, but I don't understand how.

I've read a lot of SO answers and github issues but I am still confused. Could you help me to figure out?


Solution

  • After investigation and communicating in comments I've found the answer. Before every API call I get the expite time and according to the result update access_token or return existing:

    [HttpGet("config/accesstoken")]
    public async Task<string> GetOrUpdateAccessTokenAsync()
    {
        var accessToken = await HttpContext.GetTokenAsync("access_token");
        var expiredDate = DateTime.Parse(await HttpContext.GetTokenAsync("expires_at"), null, DateTimeStyles.RoundtripKind);
    
        if (!((expiredDate - DateTime.Now).TotalMinutes < 1))
        {
            return accessToken;
        }
    
        lock (LockObject)
        {
            if (_expiredAt.HasValue && !((_expiredAt.Value - DateTime.Now).TotalMinutes < 1))
            {
                return accessToken;
            }
    
            var response = DiscoveryClient.GetAsync(_identitySettings.Authority).Result;
            if (response.IsError)
            {
                throw new Exception(response.Error);
            }
    
            var tokenClient = new TokenClient(response.TokenEndpoint, _identitySettings.Id, _identitySettings.Secret);
            var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
    
            var tokenResult = tokenClient.RequestRefreshTokenAsync(refreshToken).Result;
            if (tokenResult.IsError)
            {
                throw new Exception();
            }
    
            accessToken = tokenResult.AccessToken;
            var idToken = HttpContext.GetTokenAsync("id_token").Result;
    
            var tokens = new List<AuthenticationToken>
            {
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.IdToken,
                    Value = idToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.AccessToken,
                    Value = accessToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.RefreshToken,
                    Value = tokenResult.RefreshToken
                }
            };
    
            var expiredAt = DateTime.UtcNow.AddSeconds(tokenResult.ExpiresIn);
            tokens.Add(new AuthenticationToken
            {
                Name = "expires_at",
                Value = expiredAt.ToString("o", CultureInfo.InvariantCulture)
            });
    
            var info = HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme).Result;
            info.Properties.StoreTokens(tokens);
                HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, info.Principal, info.Properties).Wait();
    
                _expiredAt = expiredAt.ToLocalTime();
            }
    
            return accessToken;                                                 
        }
    }
    

    I call this method to get the access_token and add int to the API call headers:

    private async getAuthenticationHeader(): Promise<any> {
        return axios.get('config/accesstoken').then((response: any) => {
            return { headers: { Authorization: `Bearer ${response.data}` } };
        });
    }
    
    async getAsync<T>(url: string): Promise<T> {
        return this.axios
            .get(url, await this.getAuthenticationHeader())
            .then((response: any) => response.data as T)
            .catch((err: Error) => {
                console.error(err);
                throw err;
            });
    }
    

    Double check locking were implemented to prevent simultamious async API calls try to change access_token at the same time. Optionally you can cashe you access_token into static variable or cache, it is up to you.

    If you have any advices or alternatives it would be insteresting to discuss. Hope it helps someone.