Getting started overview

IdentityServer functions as a customizable OpenID Connect and OAuth 2.0 framework for .NET applications, allowing developers to build their own security token service. Unlike some commercial identity providers, IdentityServer requires developers to host and configure the service themselves, providing extensive control over the authentication and authorization flows (IdentityServer documentation home).

To get started, the core process involves:

  1. Setting up the IdentityServer host: This typically involves a .NET web application project configured with IdentityServer.
  2. Defining API resources and clients: You must configure which APIs IdentityServer will protect and which client applications are allowed to request tokens.
  3. Creating a client application: A .NET client application (e.g., a console app or web app) that requests tokens from your IdentityServer instance.
  4. Making an authenticated request: Using the procured token to access a protected API.

The following table provides a quick reference for the steps to get IdentityServer operational:

Step What to do Where
1. Environment Setup Install .NET SDK and create a new Web API project. Local development machine / IDE
2. Install IdentityServer Add Duende IdentityServer NuGet packages to the project. IdentityServer host project
3. Configure IdentityServer Define clients, API scopes, and users in Program.cs or a configuration class. IdentityServer host project
4. Configure API Set up a protected API to validate tokens issued by IdentityServer. Separate .NET Web API project
5. Create Client Develop a client application (e.g., console app) to request tokens. Separate .NET client project
6. Request Token Implement OAuth 2.0 Client Credentials or Authorization Code flow. Client application
7. Access Protected API Include the access token in the Authorization header of API requests. Client application

Create an account and get keys

IdentityServer is not a hosted service that requires account creation on a third-party platform. Instead, you integrate IdentityServer directly into your .NET application. The "keys" in this context refer to client secrets, API secrets, and signing credentials (e.g., X.509 certificates), which you define and manage within your IdentityServer host application (IdentityServer client configuration overview).

Step 1: Set up the IdentityServer host project

First, create a new ASP.NET Core Web API project. This project will serve as your IdentityServer instance.

dotnet new webapi -n MyIdentityServerHost
cd MyIdentityServerHost

Install the necessary Duende IdentityServer NuGet package:

dotnet add package Duende.IdentityServer

Step 2: Configure IdentityServer in Program.cs

Modify your Program.cs file to add IdentityServer services and middleware. This includes defining clients and API resources.

using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddIdentityServer()
    .AddInMemoryClients(Config.Clients)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddInMemoryApiResources(Config.ApiResources)
    .AddTestUsers(Config.TestUsers)
    .AddDeveloperSigningCredential(); // For development only, use real certs in production

builder.Services.AddControllersWithViews();

var app = builder.Build();

app.UseIdentityServer(); // This needs to be before UseAuthorization
app.UseAuthorization();
app.MapControllers();

app.Run();

public static class Config
{
    public static IEnumerable<Client> Clients => new List<Client>
    {
        new Client
        {
            ClientId = "client",
            ClientSecrets = { new Secret("secret".Sha256()) },

            AllowedGrantTypes = GrantTypes.ClientCredentials,
            AllowedScopes = { "api1" }
        }
    };

    public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope>
    {
        new ApiScope("api1", "My API")
    };

    public static IEnumerable<ApiResource> ApiResources => new List<ApiResource>
    {
        new ApiResource("api1", "My API") { Scopes = { "api1" } }
    };

    public static List<TestUser> TestUsers => new List<TestUser>
    {
        // Add test users if needed for interactive flows
    };
}

In this configuration:

  • AddInMemoryClients: Defines a client named "client" with a secret "secret". It uses the client credentials grant type and is allowed to access the "api1" scope.
  • AddInMemoryApiScopes: Defines an API scope named "api1".
  • AddInMemoryApiResources: Defines an API resource named "api1" that protects the "api1" scope.
  • AddDeveloperSigningCredential(): Generates a temporary key for token signing. This should only be used for development and replaced with a proper X.509 certificate in production (IdentityServer signing credentials guide).

Step 3: Create a protected API (optional but recommended for testing)

Create another ASP.NET Core Web API project that will be protected by IdentityServer. This API will validate the tokens issued by your IdentityServer instance.

dotnet new webapi -n MyProtectedApi
cd MyProtectedApi

Install the JWT bearer authentication package:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Configure it in Program.cs of your MyProtectedApi project:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:5001"; // URL of your IdentityServer host
        options.Audience = "api1"; // The API resource name defined in IdentityServer
        options.RequireHttpsMetadata = false; // Only for dev, set to true in production

        // If your IdentityServer is NOT on HTTPS, or if you're having certificate issues
        // options.BackchannelHttpHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = delegate { return true; } };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiScope", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("scope", "api1"); // Requires the api1 scope
    });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers().RequireAuthorization("ApiScope");

app.Run();

Add a simple controller to MyProtectedApi, e.g., WeatherForecastController.cs, ensuring it requires authorization:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace MyProtectedApi.Controllers;

[ApiController]
[Route("[controller]")]
[Authorize(Policy = "ApiScope")] // Apply the policy requiring the 'api1' scope
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Your first request

Now, create a client application to request a token from your IdentityServer and then use that token to call the protected API.

Step 1: Create a client application

Create a new console application:

dotnet new console -n MyClientApp
cd MyClientApp

Install the IdentityModel NuGet package to simplify token requests:

dotnet add package IdentityModel

Step 2: Request an access token and call the API

Modify Program.cs in MyClientApp:

using IdentityModel.Client;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace MyClientApp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // 1. Discover IdentityServer endpoints
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001"); // Your IdentityServer URL
            if (disco.IsError)
            {
                Console.WriteLine($"Error discovering IdentityServer: {disco.Error}");
                return;
            }

            // 2. Request token using client credentials flow
            var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = disco.TokenEndpoint,
                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            });

            if (tokenResponse.IsError)
            {
                Console.WriteLine($"Error requesting token: {tokenResponse.Error}");
                return;
            }

            Console.WriteLine($"Access Token: {tokenResponse.AccessToken}");

            // 3. Call protected API
            var apiClient = new HttpClient();
            apiClient.SetBearerToken(tokenResponse.AccessToken);

            var response = await apiClient.GetAsync("https://localhost:7001/WeatherForecast"); // URL of your protected API
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine($"Error calling API: {response.StatusCode} {response.ReasonPhrase}");
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"API Response: {content}");
            }

            Console.ReadKey();
        }
    }
}

Before running MyClientApp, ensure both your MyIdentityServerHost and MyProtectedApi projects are running. You can run them simultaneously from multiple terminal windows or configure your IDE (e.g., Visual Studio) to launch multiple startup projects.

When you run MyClientApp, it will:

  1. Discover the IdentityServer endpoints.
  2. Request an access token using the defined client_id and client_secret, specifying the api1 scope.
  3. If successful, it will receive an access token.
  4. It will then use this access token in the Authorization header to call the /WeatherForecast endpoint of your MyProtectedApi.
  5. The API will validate the token against your IdentityServer and return the weather forecast if the token is valid and contains the required scope.

This sequence demonstrates a complete OAuth 2.0 client credentials flow with IdentityServer acting as the authorization server and a custom API as the resource server. For more details on OAuth 2.0 flows, refer to the OAuth 2.0 specification.

Common next steps

After successfully completing your first authenticated request with IdentityServer, consider these common next steps:

  • Database integration: Replace in-memory configuration with a persistent store (e.g., SQL Server, PostgreSQL) for clients, resources, and operational data. IdentityServer supports various database providers (IdentityServer operational data documentation).
  • User authentication: Implement interactive user authentication (e.g., username/password, external identity providers) using IdentityServer's support for OpenID Connect and ASP.NET Core Identity (IdentityServer UI customization guide).
  • Deployment considerations: Plan for production deployment, including using proper X.509 certificates for token signing, securing sensitive configuration (client secrets), and configuring HTTPS.
  • Logging and monitoring: Integrate robust logging and monitoring to track authentication events and potential security issues.
  • Advanced scenarios: Explore features like refresh tokens, introspection, reference tokens, and different grant types to suit your application's specific security requirements (IdentityServer refresh tokens documentation).
  • Front-end integration: If building a Single Page Application (SPA), investigate the Authorization Code with PKCE flow, which is recommended for public clients (OAuth 2.0 PKCE specification).

Troubleshooting the first call

Encountering issues during the initial setup is common. Here are some troubleshooting tips:

  • Certificate errors (HTTPS): If you encounter certificate validation errors (e.g., "The SSL connection could not be established"), ensure your IdentityServer and API projects are configured for HTTPS and their certificates are trusted. In development, you might temporarily set RequireHttpsMetadata = false on the JWT Bearer options (as shown above), but this is not secure for production. You can also run dotnet dev-certs https --trust to trust the ASP.NET Core development certificate.
  • Mismatched URLs/Ports: Double-check that the Authority in your API and the IdentityServer URL in your client application precisely match the URL where your IdentityServer host is running (e.g., https://localhost:5001). Similarly, verify the API URL in your client.
  • Client/Scope Mismatch: Ensure the ClientId, ClientSecret, and Scope requested by your client exactly match the definitions in your IdentityServer's Config.Clients and Config.ApiScopes. Case sensitivity can be a factor.
  • API Resource Audience: Verify that the Audience configured in your protected API's JWT Bearer options matches the name of the ApiResource defined in IdentityServer (e.g., "api1").
  • Order of Middleware: In both IdentityServer and the protected API, ensure app.UseAuthentication() is called before app.UseAuthorization(), and app.UseIdentityServer() is called appropriately in the IdentityServer host.
  • Logging: Enable verbose logging for IdentityServer and your client/API applications to get detailed error messages. In appsettings.Development.json, you can set "Microsoft": "Debug" or "Duende.IdentityServer": "Debug" to increase log verbosity.
  • Firewall issues: Ensure no firewall rules are blocking communication between your client, IdentityServer, and protected API, especially if they are running on different machines or containers.
  • Token expiration: Access tokens have a limited lifetime. If you're encountering issues after a period of inactivity, your token might have expired. Implement refresh token mechanisms for long-lived sessions if required.