The Client Credentials flow is the simplest OAuth 2.0 flow for machine-to-machine authorization. There is no user context — a client application exchanges its own credentials for an access token and uses that token to call a protected API.
Steps
- Register a client (who accesses the API) and an API resource in the Identity Server
- The client calls the token endpoint with its
client_idandclient_secret - The Identity Server returns an access token
- The client sends the access token as a
Bearerheader when calling the protected API - The API validates the token against the Identity Server and authorizes the request
A. Identity Server — Config
Define the API resource and the client in memory. In production, use a database-backed client and resource store (covered in the Customizing IdentityServer4 article):
// Config.cs — in-memory clients and API resources for the Identity Server
public static class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
{
Scopes = { new Scope("api1") }
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "console_client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("my_secret".Sha256()) },
AllowedScopes = { "api1" },
}
};
}
}B. Identity Server — Startup
Wire up the Identity Server middleware in a standard ASP.NET Core Web API project:
// Startup.cs — Identity Server configuration (.NET Core 3)
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential() // dev only; use real cert in production
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseIdentityServer();
}C. Protected API
A separate ASP.NET Core project acts as the resource server. It adds JwtBearer authentication, specifying the Identity Server as the authority and api1 as the required audience. Every incoming token is automatically validated on each request:
// The protected API project — adds JWT Bearer authentication
// Startup.ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001"; // Identity Server URL
options.RequireHttpsMetadata = false;
options.Audience = "api1"; // must match ApiResource name
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
// Controller — require authentication via [Authorize]
[ApiController]
[Route("[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
public class ValuesController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new[] { "value1", "value2" });
}D. Console Client
The console app uses the IdentityModel package to discover the token endpoint, exchange the client credentials for an access token, and then call the protected API using that token as a Bearer header:
// Console client — exchange client credentials for a token, then call the API
// NuGet: IdentityModel
static async Task Main()
{
using var httpClient = new HttpClient();
// 1. Discover endpoints from the Identity Server
var disco = await httpClient.GetDiscoveryDocumentAsync(
new DiscoveryDocumentRequest
{
Address = "https://localhost:5001",
Policy = { RequireHttps = false }
});
if (disco.IsError)
{
Console.WriteLine(quot;Discovery error: {disco.Error}");
return;
}
// 2. Request an access token using client credentials
var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "console_client",
ClientSecret = "my_secret",
Scope = "api1",
});
if (tokenResponse.IsError)
{
Console.WriteLine(quot;Token error: {tokenResponse.Error}");
return;
}
Console.WriteLine(quot;Access token: {tokenResponse.AccessToken}");
// 3. Call the protected API with the Bearer token
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("https://localhost:6001/values");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(quot;API error: {response.StatusCode}");
return;
}
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(quot;Response: {content}");
}