Securing Your APIs with Duende IdentityServer Scopes and HostProfileService

Designing a robust and secure API surface is often a complex undertaking. Duende IdentityServer offers powerful tools to simplify this process, notably through the use of API scopes. These scopes, combined with components like HostProfileService, provide granular control over API access and enhance the security posture of your applications.

The foundation of API security in OAuth 2.0 lies in the concept of scopes. Initially defined as “the scope of access” a client requests, scopes are essentially identifiers that represent specific permissions or functionalities. While the OAuth 2.0 specification itself leaves the structure and semantics of scopes open for definition, Duende IdentityServer provides the framework to implement a well-structured and meaningful scope system.

Let’s delve into how scopes work in Duende IdentityServer, starting with the basics and then exploring more advanced scenarios involving HostProfileService.

Understanding API Scopes

Imagine a system with fundamental operations like read, write, and delete. In Duende IdentityServer, you can represent these operations as API scopes using the ApiScope class. This allows you to define each operation as a distinct permission that can be granted to clients.

public static IEnumerable<ApiScope> GetApiScopes()
{
    return 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.")
    };
}

This code snippet demonstrates how to define three API scopes: read, write, and delete. Each scope is created with a unique name and a user-friendly display name. Once defined, these scopes can be assigned to different clients based on their required access levels.

var webViewer = new Client
{
    ClientId = "web_viewer",
    AllowedScopes = { "openid", "profile", "read" }
};

var mobileApp = new Client
{
    ClientId = "mobile_app",
    AllowedScopes = { "openid", "profile", "read", "write", "delete" }
};

In this example, web_viewer client is granted only the read scope, while mobile_app client is given broader access with read, write, and delete scopes. This illustrates how scopes enable fine-grained control over client permissions.

Scope-Based Authorization and Access Tokens

When a client requests access to an API, it specifies the desired scopes. Duende IdentityServer then checks if these scopes are allowed for the client (based on configuration) and if the user consents to these scopes. If authorized, the requested scopes are included in the issued access token as claims of type scope.

For instance, an access token issued to mobile_app client with read, write, and delete scopes might look like this (simplified JWT example):

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

Alt text: Example of a JWT access token payload showing the ‘scope’ claim, which lists granted API scopes like ‘read’, ‘write’, and ‘delete’. This demonstrates how scopes are embedded in tokens for authorization.

The API resource server, upon receiving this access token, can inspect the scope claim to determine the client’s authorized actions. This mechanism ensures that clients can only access functionalities corresponding to their granted scopes, enhancing API security.

It’s crucial to understand that scopes primarily authorize clients, not users. Granting a write scope to a client means the client is permitted to invoke write operations on the API. User-level permissions, which determine if a specific user is allowed to perform those operations, are a separate layer of authorization that needs to be implemented within your API logic. OAuth focuses on client authorization; user authorization is application-specific.

Enhancing Access Tokens with User Claims via Scopes

API scopes can also be used to enrich access tokens with user-related information. By associating user claims with specific scopes, you can instruct Duende IdentityServer to include these claims in the access token when the scope is granted.

Consider the following scope definition:

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

Here, the write scope is configured to include the user_level claim in the access token. When a client is granted the write scope, Duende IdentityServer will request the user_level claim from the user’s profile data and include it in the access token. This allows the API to receive additional user context based on the granted scopes, which can be used for further authorization decisions or business logic within the API itself.

Parameterized Scopes and the Role of HostProfileService

In more complex scenarios, you might need scopes with parameters. For example, you might want to define scopes like transaction:id or read_patient:patientid, where id and patientid are dynamic parameters. Duende IdentityServer allows you to implement parameterized scopes using the IScopeParser interface.

By creating a custom scope parser, you can define the structure of your parameterized scopes and extract the parameter values at runtime. The DefaultScopeParser in Duende IdentityServer provides a good starting point for creating custom parsers.

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))
        {
            // we get in here with a scope like "transaction:something"
            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)
        {
            // we get in here with a scope not like "transaction"
            base.ParseScopeValue(scopeContext);
        }
        else
        {
            // we get in here with a scope exactly "transaction", which is to say we're ignoring it
            // and not including it in the results
            scopeContext.SetIgnore();
        }
    }
}

This ParameterizedScopeParser example demonstrates how to parse scopes that follow the transaction:parameter format. It extracts the parameter value and makes it available within the Duende IdentityServer pipeline. This is where HostProfileService comes into play.

The HostProfileService, implementing IProfileService, is responsible for fetching user profile data and constructing claims to be included in tokens. When dealing with parameterized scopes, HostProfileService can leverage the parsed scope values to dynamically enrich the user’s claims.

public class HostProfileService : IProfileService
{
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var transaction = context.RequestedResources.ParsedScopes.FirstOrDefault(x => x.ParsedName == "transaction");
        if (transaction?.ParsedParameter != null)
        {
            context.IssuedClaims.Add(new Claim("transaction_id", transaction.ParsedParameter));
        }
    }
}

Alt text: Diagram illustrating the interaction with HostProfileService during token issuance. It shows how HostProfileService enriches the access token with claims based on user profile data and requested scopes, highlighting its role in customizing token content.

In this HostProfileService example, we retrieve the parsed transaction scope from the RequestedResources.ParsedScopes collection. If a parameter is present (e.g., transaction:123), we extract the parameter value and add a transaction_id claim to the access token. This showcases how HostProfileService can utilize the parsed information from parameterized scopes to provide context-aware claims in the issued tokens. This mechanism is central to using Duende Identity Server Hostprofileservice effectively in scenarios requiring dynamic scope parameters.

Conclusion

API scopes are a fundamental building block for securing APIs with Duende IdentityServer. They provide a flexible and granular way to control client access to API functionalities. By understanding how to define, assign, and utilize scopes, especially in conjunction with custom components like HostProfileService and parameterized scope parsing, you can build robust and secure APIs that meet the complex requirements of modern applications. Duende IdentityServer’s comprehensive scope management capabilities empower developers to design secure and well-structured API surfaces, ensuring that only authorized clients can access the intended resources and operations.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *