Table of Contents

Clients and resources

What a server issues tokens for, and to whom, is three lists: the identity data it can release, the APIs it protects, and the client applications allowed to ask. The code on this page is included from samples/ClientsAndResources, which a test boots and asks for a token as the machine client below.

Identity resources

/// <summary>The identity data a client may ask about the user, each scope with the claims it releases.</summary>
public static IEnumerable<IdentityResource> IdentityResources =>
[
    // "openid" is required for any OpenID Connect request: it releases the subject id, "sub".
    new IdentityResources.OpenId(),
    new IdentityResources.Profile(),
    new IdentityResources.Email(),

    // A scope of your own, releasing claims of your own.
    new IdentityResource("employee", "Your employee record", ["employee_id", "department"]),
];

An IdentityResource is a scope a client requests to learn something about the user, and the claims that scope releases into the identity token or from the userinfo endpoint. IdentityResources holds the standard OpenID Connect ones; openid is required for an OpenID Connect request at all. Which claims a user actually has is the profile service's business.

API scopes and API resources

/// <summary>What a client may ask to do: one scope per permission an API checks.</summary>
public static IEnumerable<ApiScope> ApiScopes =>
[
    new ApiScope("orders.read", "Read your orders"),
    new ApiScope("orders.write", "Place orders") { Emphasize = true },
];

An ApiScope is a permission a client can be granted and an API checks for. Emphasize marks it on the consent screen.

/// <summary>The APIs: each names the scopes it accepts and becomes the audience of tokens carrying them.</summary>
public static IEnumerable<ApiResource> ApiResources =>
[
    new ApiResource("https://orders.example.com", "Orders API")
    {
        Scopes = { "orders.read", "orders.write" },

        // Lets the API authenticate at the introspection endpoint, to read reference tokens.
        ApiSecrets = { new Secret("orders-api-secret".Sha256()) },
    },
];

An ApiResource groups scopes under one API. Its name becomes the aud of every access token carrying one of them, so the API can check that a token was issued for it; its ApiSecrets let it call the introspection endpoint. A server whose APIs check only scopes can register scopes alone.

A web application: authorization code with PKCE

// A server-side web application (or a BFF): authorization code with PKCE, a client secret, and a refresh
// token so it can renew the user's access token without sending them back here.
new Client
{
    ClientId = "orders-web",
    ClientName = "Orders",
    ClientSecrets = { new Secret("orders-web-secret".Sha256()) },

    AllowedGrantTypes = { OidcConstants.GrantTypes.AuthorizationCode },
    RequirePkce = true,           // the default, stated
    AllowOfflineAccess = true,    // issue refresh tokens

    RedirectUris = { "https://orders.example.com/signin-oidc" },
    PostLogoutRedirectUris = { "https://orders.example.com/signout-callback-oidc" },
    BackChannelLogoutUri = "https://orders.example.com/bff/backchannel",

    AllowedScopes = { "openid", "profile", "email", "orders.read", "orders.write", "offline_access" },
    AccessTokenLifetime = 900,    // seconds
},

The flow for anything a user signs in to. RequirePkce is on by default, as OAuth 2.1 requires; the implicit and password grants do not exist here. Redirect URIs are compared exactly.

PKCE is checked again when the code is redeemed, as Duende 8.0.1's token request validator checks it, measured: when the client requires PKCE or the code was issued with a code_challenge, the request must carry the matching code_verifier; and a code_verifier sent for a code issued without a challenge is refused invalid_grant, since 0.176.0 — the PKCE downgrade of RFC 9700 §4.8.2, in which an attacker's authorization response, obtained without a challenge, is injected into a session that made its own. Until 0.176.0 such a code redeemed with any verifier, and a code without a challenge redeemed for a client requiring PKCE.

Security — clients that send a verifier without a challenge. A client registered with RequirePkce = false that sends code_verifier at the token endpoint but no code_challenge at the authorize endpoint is now refused. Send both — PKCE protects a confidential client too — or neither. AllowOfflineAccess issues a refresh token, which is single use by default, so each refresh returns a new one. A browser-only application with no back end registers the same way with RequireClientSecret = false.

A service: client credentials

// A back-end service calling the API as itself: client credentials, no user involved.
new Client
{
    ClientId = "orders-sync",
    ClientName = "Order synchronisation job",
    ClientSecrets = { new Secret("orders-sync-secret".Sha256()) },

    AllowedGrantTypes = { OidcConstants.GrantTypes.ClientCredentials },
    AllowedScopes = { "orders.read" },
},

No user and no browser: the client authenticates with its secret and receives an access token for the scopes it is allowed. Client libraries shows the calling side.

Registering them

builder.Services
    .AddIdentityServer(options => options.KeyManagement.Enabled = false)
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddInMemoryApiResources(Config.ApiResources)
    .AddInMemoryClients(Config.Clients)
    .AddSigningCredential(new RsaSecurityKey(RSA.Create(2048)) { KeyId = "development" }, SecurityAlgorithms.RsaSha256);

The AddInMemory… methods on IdentityServerBuilderExtensionsCore register the lists as the server's stores. Each client is validated when it is read, and one whose registration cannot work — a grant it may not combine, a redirect URI that is not a location — is refused and logged rather than served. To keep them in a database instead, see persistence.