Skip to content

Isolation Sample

Imagine a set of services with separate APIs for handling orders and tracking inventory, an Orders API and Inventory API. Each has their own distinct set of API scopes, plus a set of scopes shared between the APIs. In addition, there’s a global scope used by legacy systems that haven’t been updated yet to use Resource Isolation. The set of scopes used by each application are:

urn:ordersurn:inventoryNot Shared with any API Resource
orders.readinventory.readglobal.audit
orders.writeinventory.write
shared.readshared.read

The below code creates in-memory scopes, API resources, and a single client (which knows about the aforementioned resources) inside a Duende IdentityServer application. Notice that all scopes are created in a single Scopes collection, then the Resources collection groups the scopes per ApiResource. Finally, the Client includes all scopes in its AllowedScopes property because the client will be requesting any combination of those scopes from Duende IdentityServer. The only grouping happening is when the ApiResource objects link an API resource to a scope.

Config.cs
// All scopes used by all API Resources and Clients
public static readonly IEnumerable<ApiScope> Scopes = [
// resource specific scopes
new ApiScope("orders.read"), new ApiScope("orders.write"),
new ApiScope("inventory.read"), new ApiScope("inventory.write"),
// a scope shared by multiple resources
new ApiScope("shared.read"),
// scopes without resource association
new ApiScope("global.audit"),
];
// API resources with the scopes they use
public static readonly IEnumerable<ApiResource> Resources = [
new ApiResource("urn:orders", "Orders API")
{ Scopes = { "orders.read", "orders.write", "shared.read" } },
new ApiResource("urn:inventory", "Inventory API") {
Scopes = { "inventory.read", "inventory.write", "shared.read" },
RequireResourceIndicator = true
} ];
public static readonly IEnumerable<Client> Clients = [
new Client {
ClientId = "resource.isolation.demo.client",
ClientSecrets = { new Secret("my-secret".Sha256()) },
ClientClaimsPrefix = "",
AllowedGrantTypes = GrantTypes.ClientCredentials,
// Client is allowed to access all scopes for all ApiResources
AllowedScopes =
{
"orders.read", "orders.write",
"inventory.read", "inventory.write",
"shared.read",
"global.audit",
}
}
];

When requesting an ApiResource, IdentityServer will create a token with scopes filtered to what is supported by that ApiResource. Scopes are not owned by any individual ApiResource, and are global across your applications because internally they’re an arbitrary string. An ApiResource doesn’t “own” scopes, it is allowed access to those scopes.

The table below shows the resulting audience claim (aud) when making requests for a token with a specific scope/resource combination.

ScopesResource ApiResult audience claim (aud)
orders.readnullurn:orders
inventory.readnullNOT SET
inventory.readurn:inventoryurn:inventory
orders.read global.auditnullurn:orders
shared.readnullurn:orders
orders.read shared.readnullurn:orders

The code for the above scenario is written out in the two tabs below. Each tab is a C# file-based app. One is a Duende IdentityServer application with scopes, API resources, and a client. The second app is a console client that makes requests to Duende IdentityServer, each request with different combinations of scopes and resources to show the result aud claim. To help understand how resource isolation works, feel free to run the two apps locally and make modifications as you see fit to experiment.

IdentityServer.cs
// Run with `dotnet run IdentityServer.cs`
#:sdk Microsoft.Net.Sdk.Web
#:property PublishAot=false
#:package Duende.IdentityServer@8.0.0-alpha.1
using Duende.IdentityServer.Models;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("https://localhost:5001");
_ = builder.Services.AddIdentityServer(options =>
{
// emits static audience if required
options.EmitStaticAudienceClaim = false;
// control format of scope claim
options.EmitScopesAsSpaceDelimitedStringInJwt = true;
})
.AddInMemoryApiScopes(InMemoryConfig.Scopes)
.AddInMemoryApiResources(InMemoryConfig.Resources)
.AddInMemoryClients(InMemoryConfig.Clients);
var app = builder.Build();
app.UseIdentityServer();
app.Run();
public static class InMemoryConfig
{
// All scopes used by all API Resources and Clients
public static readonly IEnumerable<ApiScope> Scopes = [
// resource specific scopes
new ApiScope("orders.read"), new ApiScope("orders.write"),
new ApiScope("inventory.read"), new ApiScope("inventory.write"),
// a scope shared by multiple resources
new ApiScope("shared.read"),
// scopes without resource association
new ApiScope("global.audit"),
];
// API resources with the scopes they use
public static readonly IEnumerable<ApiResource> Resources = [
new ApiResource("urn:orders", "Orders API")
{ Scopes = { "orders.read", "orders.write", "shared.read" } },
new ApiResource("urn:inventory", "Inventory API") {
Scopes = { "inventory.read", "inventory.write", "shared.read" },
RequireResourceIndicator = true
}, ];
public static readonly IEnumerable<Client> Clients = [
new Client {
ClientId = "resource.isolation.demo.client",
ClientSecrets = { new Secret("my-secret".Sha256()) },
ClientClaimsPrefix = "",
AllowedGrantTypes = GrantTypes.ClientCredentials,
// Client is allowed to access all scopes for all ApiResources
AllowedScopes =
{
"orders.read", "orders.write",
"inventory.read", "inventory.write",
"shared.read",
"global.audit",
}
}
];
}

The code above outputs the token response for each request to Duende IdentityServer. Below is that output, but modified to be in a table to simplify

ScopesResource ApiResult audience claim (aud)
orders.readnullurn:orders
inventory.readnullNOT SET
inventory.readurn:inventoryurn:inventory
orders.read global.auditnullurn:orders
shared.readnullurn:orders
orders.read shared.readnullurn:orders