There's a good chance your application consumes one or more APIs. For example, you may have a back-office application that works with a shipping API and an invoice API. Or perhaps you have a microservice architecture, and there are 50 different APIs involved.
In this landscape, one of the most persistent security anti-patterns we see is having access tokens with too much access. An overprivileged token occurs when a client requests a wide array of scopes, for example, invoice.read, shipping.write, and, and receives a single access token that contains all the issued claims.
While asking for multiple scopes at once can be convenient, the issued token raises a concerning trust issue. If the shipping API is compromised and the token is leaked, an attacker can use it to access the invoice API. The attacker has a token that’s issued once, but usable against almost every service within a solution. We’ve sacrificed security for convenience, which can weaken our security posture.
This is where Resource Isolation comes in. Based on RFC 8707 (Resource Indicators for OAuth 2.0), this feature allows you to enforce strict trust boundaries between your APIs, ensuring that a token is only valid for the specific target it was intended for.
The Problem With Audience Ambiguity
In OAuth 2.0, the aud (audience) claim tells an API, "this token is for you." However, in many default implementations, the audience is either a single value shared by all APIs, or a list of all valid audiences.
If you have a large system, your token might look like this:
{
"iss": "https://identity.duendesoftware.com",
"aud": ["invoice_api", "shipping_api", "inventory_api"],
"scope": ["invoice.read", "shipping.write", "inventory.read"]
}
This token is valid at three different services: invoice_api, shipping_api, and inventory_api. This violates the Principle of Least Privilege. In a Zero Trust architecture, we want to ensure that a token sent to the Shipping API is unusable at the Invoice API.
OAuth And Resource Indicators (RFC 8707)
Resource Isolation forces a change in how we think about requesting access. Instead of just asking for "what I want to do" (Scopes), the client must also specify "where I want to do it" (Resources).
When Resource Isolation is enabled, the IdentityServer engine performs a crucial check: it calculates the intersection of the requested scopes and the requested resource. The resulting access token is "downscoped" to contain only the permissions relevant to the resource(s) you requested.
Configuring Resource Isolation (Server-Side)
Configuring Resource Isolation in Duende IdentityServer requires the Enterprise plan, and configuration updates to shift from defining ApiScopes to grouping them under ApiResources.
Here is how you configure a resource to enforce this isolation in your Config.cs. Note that while the resource id in this code snippet is a Uniform Resource Name (URN), you can use any type of identifier here.
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource("urn:invoice-service", "Invoice Microservice")
{
// These scopes are now exclusively bound to this resource
Scopes = { "invoice.read", "invoice.pay" },
// This turns on the requirement for resource isolation
RequireResourceIndicator = true
},
new ApiResource("urn:shipping-service", "Shipping Microservice")
{
Scopes = { "shipping.write" },
RequireResourceIndicator = true
}
};
By setting RequireResourceIndicator = true, you are telling IdentityServer to reject any token request that includes these scopes unless the client explicitly specifies the target resource.
Client Implementation
Enforcing resource isolation on the server is only half the battle. Your clients must now explicitly signal which resource they intend to access. If they don't, IdentityServer will reject the request with an invalid_target error.
Machine-to-Machine (Worker)
For background workers or daemons using the Client Credentials flow, you typically use the Duende.IdentityModel library.
The ClientCredentialsTokenRequest object includes a Resource collection where you can add any number of resource indicators:
using Duende.IdentityModel.Client;
var client = new HttpClient();
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = "https://demo.duendesoftware.com/connect/token",
ClientId = "invoice_worker",
ClientSecret = "secret",
// The scope defines the permission
Scope = "invoice.read",
// The parameter defines the target (RFC 8707)
Resource = [ "urn:invoice-service" ]
});
In this example, the resulting access token will contain only the urn:invoice-service audience. If the worker later needs to talk to the shipping service, it must make a separate token request targeting urn:shipping-service.
Interactive Web Apps and ASP.NET Core
For user-facing applications using the standard OpenID Connect handler in ASP.NET Core, you can utilize the Resource property directly on the OpenIdConnectOptions. It ensures the resource parameter is sent not only during the initial redirect to the login page but also during the back-channel exchange of the authorization code for the access token.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://demo.duendesoftware.com";
options.ClientId = "interactive_app";
options.ClientSecret = "secret";
// Standard scopes
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("invoice.read");
// Explicitly set the target resource here
options.Resource = "urn:invoice-service";
options.ResponseType = "code";
options.SaveTokens = true;
});
Keep in mind that while the RFC allows multiple resource parameters, the Microsoft OpenID Connect handler only supports a single resource value here.
Note the Resource property can also be set as a parameter in the OnRedirectToIdentityProvider event, which may be helpful if you need to execute custom code and logic to determine the resource, for example, in multi-tenant environments:
.AddOpenIdConnect(options =>
{
// ... existing configuration ...
options.Events.OnRedirectToIdentityProvider = context =>
{
var tenantSpecificResource = DetermineResource(context);
// Overwrite or set the 'resource' parameter
context.ProtocolMessage.SetParameter("resource", tenantSpecificResource);
return Task.CompletedTask;
};
});
Summary
Adopting Resource Isolation is one of the highest-value changes you can make to harden your security posture. You will generally see smaller tokens, and benefit from a reduced blast radius when a token is compromised. In addition, you can audit which resource a token was generated for, which may help with compliance needs.
While resource isolation introduces some complexity in client-side state management (requiring management of multiple tokens rather than one), the security benefits for enterprise-scale systems are undeniable.
If you are building an architecture that consists of multiple services and APIs working together, we highly recommend evaluating Resource Isolation in Duende IdentityServer to ensure trust boundaries between services are respected.