10
votes

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?

2
Btw TokenClient is now obsolete (github.com/IdentityServer/IdentityServer4/issues/2885), you can use an extension method on the regular HttpClient. See for example github.com/IdentityServer/IdentityServer4/blob/master/samples/… - Johan Maes

2 Answers

12
votes

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;
    }
}
4
votes

In other words - you need to cache that token somehow. When you request the token, you get an ExpiresIn in the response - this will tell you how long the token will be valid.

Another option is to wait until the API returns a 401 - and then request a new token.

Refresh tokens are not used with client credentials flow.