Search code examples
c#identityserver4

Best practice for persisting tokens using Client Credentials flow


I have an ASP.NET Core MVC application allowing anonymous users. This app is calling an ASP.NET Web API that is protected by Identity Server 4. I have created a client in Identity Server describing the MVC app (client) and given it access to the api scope like this:

new Client
{
    ClientId = "my-mvc-client-app",
    AllowedGrantTypes = GrantTypes.ClientCredentials,

    RequireConsent = false,
    ClientSecrets = new List<Secret> { new Secret("this-is-my-secret".Sha256()) },
    AllowedScopes = new List<string>
    {
        StandardScopes.OpenId.Name,
        StandardScopes.Profile.Name,
        StandardScopes.OfflineAccess.Name,
        "my-protected-api"
    },
    RedirectUris = new List<string>
    {
        "http://localhost:5009/signin-oidc",
    }
}

In my MVC app, I'm using TokenClient to get a token that I can use when making requests to the protected API like this:

var disco = await DiscoveryClient.GetAsync("http://localhost:5010");
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, clientSecret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("hrmts-test-candidate-api-scope");

This works fine, but I'm requesting new tokens from Identity Server on every request, which is probably not a good idea.

What is the best practice for handling the tokens? How can I persist them on the client (the MVC app) and how can I handle refresh tokens to make sure the client gets a new token when necessary?


Solution

  • You need to wrap that client in a managed service of some kind (as a singleton) so that you can use it anywhere you need. We have a token component that we use for server to server communication that follows this flow:

    public class ServerTokenComponent
    {
        private TokenResponse Token { get; set; }
        private DateTime ExpiryTime { get; set; }
        public async Task<TokenResponse> GetToken()
        {
            //use token if it exists and is still fresh
            if (Token != null && ExpiryTime > DateTime.UtcNow)
            {    
                return Token;
            }     
    
            //else get a new token
            var client = new TokenClient("myidpauthority.com","theclientId","thesecret")
            var scopes = "for bar baz";
    
            var tokenResponse = await client.RequestClientCredentialsAsync(scopes);
    
            if (tokenResponse.IsError || tokenResponse.IsHttpError)
            {
                throw new SecurityTokenException("Could not retrieve token.");
            }
    
            //set Token to the new token and set the expiry time to the new expiry time
            Token = tokenResponse;
            ExpiryTime = DateTime.UtcNow.AddSeconds(Token.ExpiresIn);
    
            //return fresh token
            return Token;
        }
    }