Here is the barebones setup needed to protect an API using the ClientCredentials grant type and IdentityServer4.
IdentityServer Project
This project has only two files — Startup.cs and Program.cs — generated as a default .NET 6 project with a single dependency:
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
</ItemGroup>public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}The startup file defines clients and scopes in memory (do not use in-memory storage in production) and creates a client for the protected API with a hashed secret:
public class Startup
{
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "web-api",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "web-api-scope" },
AllowedCorsOrigins = new List<string>
{
"http://localhost:5000",
"https://localhost:5001"
}
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("web-api-scope", "My API")
};
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiScopes(ApiScopes)
.AddInMemoryClients(Clients);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
IdentityModelEventSource.ShowPII = true;
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseIdentityServer();
}
}Api Project
The API project must be configured to authenticate against our IdentityServer authority using the Bearer scheme. Files: Startup.cs, Program.cs, WeatherForecastController.cs.
Since multiple authentication schemes can be registered, we must specify the default with AddAuthentication(string defaultScheme) and add the Bearer scheme with .AddJwtBearer("Bearer", ...). Single dependency:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.5" />
</ItemGroup>public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthorization();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, "Bearer", options =>
{
options.Authority = Configuration["EndPoints:IdentityAuthority"];
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors(builder => builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}Test
Three NUnit tests with both services running — the third test uses the IdentityModel library to retrieve a token from the token endpoint before making the authorized call. All tests pass:
[Test]
public async Task CallAuthorizedMethod_WithNoBearer_Fails()
{
HttpClient apiClient = new();
apiClient.BaseAddress = new Uri("https://localhost:5001");
HttpRequestMessage requestMessage = new HttpRequestMessage(
HttpMethod.Get,
new Uri("api/authorized", UriKind.Relative));
HttpResponseMessage response = await apiClient.SendAsync(requestMessage);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task CallNotAuthorizedMethod_Succeeds()
{
HttpClient apiClient = new();
apiClient.BaseAddress = new Uri("https://localhost:5001");
HttpRequestMessage requestMessage = new HttpRequestMessage(
HttpMethod.Get,
new Uri("api/notauthorized", UriKind.Relative));
HttpResponseMessage response = await apiClient.SendAsync(requestMessage);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
[Test]
public async Task CallAuthorizedMethod_WithBearer_UsingIdentityModel_Succeeds()
{
var identityServerClient = new HttpClient();
DiscoveryDocumentResponse disco = await identityServerClient
.GetDiscoveryDocumentAsync("https://localhost:6001");
TokenResponse? tokenResponse = await identityServerClient
.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "web-api",
ClientSecret = "secret",
Scope = "web-api-scope"
});
HttpClient apiClient = new();
apiClient.BaseAddress = new Uri("https://localhost:5001");
HttpRequestMessage requestMessage = new HttpRequestMessage(
HttpMethod.Get,
new Uri("api/authorized", UriKind.Relative));
requestMessage.Headers.Add("Authorization", quot;Bearer {tokenResponse.AccessToken}");
HttpResponseMessage response = await apiClient.SendAsync(requestMessage);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}