Skip to content

2. The Client Credentials Grant

Matt Goldman edited this page Jul 18, 2023 · 1 revision

The client credentials grant is typically used in scenarios where a user is not present (and, therefore, nobody is around to enter a password). This is often referred to as machine-to-machine (M2M) or API-to-API communication.

In dotnetflix, the client credentials grant is used by the gateway API to obtain a JWT so that it can make authenticated calls to the Videos service and the Subscriptions service. Note that the Videos service uses an API key to talk to YouTube - this is another passwordless scenario but, while API keys are common, it's not based on an open standard and is implemented in a proprietary way by API providers.

Tokens obtained via the client credentials grant represent an application rather than a user, which in principle doesn't matter, but can be important if, for example, you log all requests made by a user.

Setting up an HTTP Handler

The client credentials grant is used by the API to obtain tokens to make authenticated calls to the Videos service and the Subscriptions service. A delegating handler is used to automatically add the JWT as a token with the Bearer scheme as an Authentication header to all HTTP requests made with a defined HTTPClient.

In Program.cs, named HTTPClients (using consts for the names) are defined that can then be injected into services as required, that are configured to use these handlers.

string subscriptionsUri = builder.Configuration.GetValue<string>("ServiceConfig:SubscriptionsClient:BaseUrl")!;
builder.Services.AddHttpClient(SubscriptionsService.SubscriptionsClient, client => client.BaseAddress = new Uri(subscriptionsUri))
    .AddHttpMessageHandler((s) => s.GetService<TokenHandler>());

string videosUri = builder.Configuration.GetValue<string>("ServiceConfig:VideosClient:BaseUrl")!;
builder.Services.AddHttpClient(VideosService.VideosClient, client => client.BaseAddress = new Uri(videosUri))
    .AddHttpMessageHandler((s) => s.GetService<TokenHandler>());

Obtaining tokens

There are various OIDC libraries available that can simplify the process of obtaining JWTs, and typically an IDP provider will offer a client library or SDK. In the case of IdentityServer, which is used in dotnetflix, the IdentityModel.OIDCClient package is provided for .NET.

However, obtaining tokens in OIDC is just achieved through the use of standard HTTP calls, so this has been done in this handler in code without the use of any libraries, to show how straightforward it is. The handler (you can see the full code here) defines a method for obtaining a JWT using the client credentials grant, which can then be added to authenticated requests.

private async Task<(string Token, DateTime Expires)> GetApiToken(ClientCredentials credentials)
{
    string token;
    var expires = DateTime.UtcNow;

    var paramVals = new Dictionary<string, string>();

    string grantType = "client_credentials";

    paramVals.Add("client_id", credentials.ClientId);
    paramVals.Add("scope", credentials.Scope);
    paramVals.Add("client_secret", credentials.ClientSecret);
    paramVals.Add("grant_type", grantType);

    var response = await _httpClient.PostAsync("/connect/token", new FormUrlEncodedContent(paramVals));

    if (response.IsSuccessStatusCode)
    {
        var stringContent = await response.Content.ReadAsStringAsync();

        var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(stringContent);

        token = tokenResponse?.access_token ?? "";

        expires = expires.AddSeconds(tokenResponse!.expires_in);
    }
    else
    {
        throw new Exception("Error getting client credentials token");
    }

    return (token, expires);
}

Breaking down the GetApiToken method

The method returns a tuple with the access token as a string and the expiry time of the token as a DateTime. Both of these are stored in fields; the expiry time is used to determine if the token is still valid, and if it is it is used in HTTP requests, and if not, this method is called again to obtain a new token. The method contains a single parameter which is of a type called ClientCredentials. This is a type I've defined that is used to bind to the required values for this client in configuration:

public class ClientCredentials
{
    public string ClientId { get; set; } = string.Empty;

    public string ClientSecret { get; set; } = string.Empty;

    public string Scope { get; set; } = string.Empty;

    public string BaseUrl { get; set; } = string.Empty;
}

The values here are defined in the IDP (in this case you can see them here). Later in this method, you can see that these are passed as form URL encoded values in the HTTP POST request:

string grantType = "client_credentials";

paramVals.Add("client_id", credentials.ClientId);
paramVals.Add("scope", credentials.Scope);
paramVals.Add("client_secret", credentials.ClientSecret);
paramVals.Add("grant_type", grantType);

var response = await _httpClient.PostAsync("/connect/token", new FormUrlEncodedContent(paramVals));

Here the grant type is also specified so that the IDP knows how to process the request. The URL to post this to can be obtained from the discovery document; again client libraries can help with this, but it's also easy enough to just obtain it yourself. Once a successful response is obtained, the response is deserialised to another type I've defined called TokenResponse, which has properties corresponding to the standard properties of an OIDC response.

public class TokenResponse
{
    public string token_type { get; set; }
    public double expires_in { get; set; }
    public string ext_expires_in { get; set; }
    public string access_token { get; set; }
}

Once the response has been deserialized, the access token and expiry are returned in the tuple.