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 = falsethat sendscode_verifierat the token endpoint but nocode_challengeat 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 withRequireClientSecret = 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.