← Files Duende SkillsARCHIVED FILE

skills/identityserver-configuration/docs/resources-scopes.md

9.96 KB · Oct 3, 2026 · 06:32 UTC

↓ Download file

# Resources and Scopes: Identity Resources, API Scopes, and API Resources

## Resource Types: The Three Pillars

IdentityServer manages access to resources through three distinct types. Understanding when to use each is fundamental.

### Decision Matrix: Which Resource Type to Use

| Need                                    | Resource Type         | Purpose                                                |
| --------------------------------------- | --------------------- | ------------------------------------------------------ |
| User identity claims (name, email)      | **Identity Resource** | Groups claims requested via `scope` parameter          |
| API access control                      | **API Scope**         | Defines what operations a client can perform           |
| API grouping, audience (`aud`), signing | **API Resource**      | Groups scopes under a logical API with shared settings |

## Identity Resources

An identity resource is a named group of claims about a user, requested using the `scope` parameter.

The `openid` scope is mandatory per the OpenID Connect spec and returns the `sub` (subject id) claim.

### Standard Identity Resources

```csharp
public static IEnumerable<IdentityResource> IdentityResources =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),   // required: returns sub claim
        new IdentityResources.Profile(),  // name, family_name, etc.
        new IdentityResources.Email(),    // email, email_verified
        new IdentityResources.Phone(),    // phone_number, phone_number_verified
        new IdentityResources.Address()   // address JSON
    };
```

### Custom Identity Resources

```csharp
public static IEnumerable<IdentityResource> IdentityResources =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResource(
            name: "profile",
            userClaims: new[] { "name", "email", "website" },
            displayName: "Your profile data")
    };
```

### Granting Access to Identity Resources

```csharp
var client = new Client
{
    ClientId = "client",
    AllowedScopes = { "openid", "profile" }
};
```

The client requests the resource via the scope parameter:

```
https://identity.example.com/connect/authorize?client_id=client&scope=openid profile
```

## API Scopes

API Scopes define the scope of access a client requests. They represent operations or permissions.

### Defining API Scopes

```csharp
public static IEnumerable<ApiScope> ApiScopes =>
    new List<ApiScope>
    {
        new ApiScope(name: "read",   displayName: "Read your data."),
        new ApiScope(name: "write",  displayName: "Write your data."),
        new ApiScope(name: "delete", displayName: "Delete your data.")
    };
```

### Scopes with User Claims

Add user claims to access tokens when a scope is granted:

```csharp
var writeScope = new ApiScope(
    name: "write",
    displayName: "Write your data.",
    userClaims: new[] { "user_level" });
```

### Scope Authorization in Tokens

When a scope is granted, it appears in the access token:

```json
{
  "typ": "at+jwt",
  "client_id": "mobile_app",
  "sub": "123",
  "scope": "read write delete"
}
```

### Important: Scopes Authorize Clients, Not Users

Scopes control what a client can do, not what a user is permitted to do. User-level authorization is application logic and not covered by OAuth.

```csharp
// ❌ WRONG mental model: "write" scope = user can write
// ✅ CORRECT mental model: "write" scope = client is allowed to invoke write operations
```

### Audience Behavior Without API Resources

When using only API Scopes (without API Resources), no `aud` claim is added to tokens. To get an audience claim, either:

- Use API Resources (recommended for multi-API systems)
- Enable `EmitStaticAudienceClaim` on the options (emits `{issuer}/resources`)

### Parameterized Scopes

Parameterized scopes carry a value alongside the scope name (e.g. `transaction:abc123`, `tenant:acme:read`). Implement `IScopeParser` — its single method is `ParsedScopesResult ParseScopeValues(IEnumerable<string> scopeValues)`. In practice, subclass `DefaultScopeParser` (the default registration) and override `ParseScopeValue(ParseScopeContext scopeContext)`.

`ParseScopeContext` members: `RawValue`, `ParsedName`, `ParsedParameter`, `Error`, `Ignore`, `Succeeded`; methods `SetParsedValues(name, parameter)`, `SetIgnore()`, `SetError(message)`.

```csharp
public class ParameterizedScopeParser : DefaultScopeParser
{
    public ParameterizedScopeParser(ILogger<DefaultScopeParser> logger) : base(logger)
    { }

    public override void ParseScopeValue(ParseScopeContext scopeContext)
    {
        const string transactionScopeName = "transaction";
        const string separator = ":";
        const string transactionScopePrefix = transactionScopeName + separator;

        var scopeValue = scopeContext.RawValue;

        if (scopeValue.StartsWith(transactionScopePrefix))
        {
            var parts = scopeValue.Split(separator, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 2)
            {
                scopeContext.SetParsedValues(transactionScopeName, parts[1]);
            }
            else
            {
                scopeContext.SetError("transaction scope missing transaction parameter value");
            }
        }
        else if (scopeValue != transactionScopeName)
        {
            base.ParseScopeValue(scopeContext);
        }
        else
        {
            scopeContext.SetIgnore();
        }
    }
}
```

Register the parser on the IdentityServer builder:

```csharp
idsvrBuilder.AddScopeParser<ParameterizedScopeParser>();
```

Read the parsed parameter downstream in an `IProfileService` via `RequestedResources.ParsedScopes`:

```csharp
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
    var transaction = context.RequestedResources.ParsedScopes
        .FirstOrDefault(x => x.ParsedName == "transaction");

    if (transaction is not null)
    {
        // transaction.ParsedParameter holds "abc123" for scope "transaction:abc123"
        context.IssuedClaims.Add(new Claim("transaction_id", transaction.ParsedParameter));
    }
}
```

## Custom Resource Validation

`IResourceValidator` validates the resources (scopes + resource indicators) a client requests at the authorize/token endpoints. The default is `DefaultResourceValidator`. Implement the interface to add cross-scope rules, tenant checks, or resource-indicator policy:

```csharp
public class CustomResourceValidator : IResourceValidator
{
    // NOTE: the CancellationToken parameter was ADDED in v8.
    public Task<ResourceValidationResult> ValidateRequestedResourcesAsync(
        ResourceValidationRequest request, CancellationToken cancellationToken)
    {
        // request.Client, request.Scopes, request.ResourceIndicators (RFC 8707)
        // ... custom validation ...
        return Task.FromResult(new ResourceValidationResult());
    }
}
```

Register on the builder:

```csharp
idsvrBuilder.AddResourceValidator<CustomResourceValidator>();
```

When **decorating/wrapping** the default validator, also register it so it can be resolved:

```csharp
services.AddTransient<DefaultResourceValidator>();
idsvrBuilder.AddResourceValidator<CustomResourceValidator>();
```

> **v8 breaking change:** `ValidateRequestedResourcesAsync` gained a `CancellationToken` parameter. Implementations written for v7 or earlier must add it to compile.

API Resources group scopes under a logical API, providing:

- JWT `aud` (audience) claim based on the resource name
- Common user claims across all contained scopes
- Introspection support via API secrets
- Per-resource signing algorithm configuration

### Defining API Resources

```csharp
public static IEnumerable<ApiScope> ApiScopes =>
    new List<ApiScope>
    {
        new ApiScope(name: "invoice.read",   displayName: "Reads your invoices."),
        new ApiScope(name: "invoice.pay",    displayName: "Pays your invoices."),
        new ApiScope(name: "customer.read",  displayName: "Reads customer information."),
        new ApiScope(name: "customer.contact", displayName: "Allows contacting customers."),
        new ApiScope(name: "manage",         displayName: "Provides administrative access."),
        new ApiScope(name: "enumerate",      displayName: "Allows enumerating data.")
    };

public static IEnumerable<ApiResource> ApiResources =>
    new List<ApiResource>
    {
        new ApiResource("invoice", "Invoice API")
        {
            Scopes = { "invoice.read", "invoice.pay", "manage", "enumerate" }
        },
        new ApiResource("customer", "Customer API")
        {
            Scopes = { "customer.read", "customer.contact", "manage", "enumerate" }
        }
    };
```

### Token Audience Examples

Requesting `invoice.read` and `invoice.pay`:

```json
{
  "aud": "invoice",
  "scope": "invoice.read invoice.pay"
}
```

Requesting `invoice.read` and `customer.read`:

```json
{
  "aud": ["invoice", "customer"],
  "scope": "invoice.read customer.read"
}
```

Requesting `manage` (shared scope):

```json
{
  "aud": ["invoice", "customer"],
  "scope": "manage"
}
```

### API Resource User Claims

Add claims to access tokens regardless of which scope is requested:

```csharp
var customerResource = new ApiResource("customer", "Customer API")
{
    Scopes = { "customer.read", "customer.contact", "manage", "enumerate" },
    UserClaims = { "department_id", "sales_region" }
};
```

### Per-Resource Signing Algorithm

```csharp
var invoiceApi = new ApiResource("invoice", "Invoice API")
{
    Scopes = { "invoice.read", "invoice.pay", "manage", "enumerate" },
    AllowedAccessTokenSigningAlgorithms = { SecurityAlgorithms.RsaSsaPssSha256 }
};
```

### Resource Isolation (Enterprise Edition)

Use the `resource` parameter (RFC 8707) to request tokens scoped to a single API resource, preventing over-privileged tokens.

```csharp
var resources = new[]
{
    new ApiResource("urn:invoices")
    {
        Scopes = { "read", "write" },
        RequireResourceIndicator = true  // audience only included when explicitly requested
    }
};
```

SHA-256: cf66c82b84fe742f9ea3f2adfb900a386d8341aa1ae1d9149e1c88595a58607d