Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

Scope Parser

The IScopeParser interface is responsible for parsing the raw scope parameter from OAuth/OIDC requests into individual, structured scope values. While the default implementation treats scopes as simple space-delimited strings, custom implementations enable parameterized scopes - scopes that carry dynamic data like transaction IDs, tenant identifiers, or resource-specific parameters.

Use a custom scope parser when:

  • Scopes carry runtime parameters (e.g., transaction:abc123, tenant:acme)
  • You need to validate scope structure before further processing
  • Scope values follow a convention that requires extraction of embedded data

Duende.IdentityServer.Validation.IScopeParser

Section titled “Duende.IdentityServer.Validation.IScopeParser”
/// <summary>
/// Allows parsing raw scopes values into structured scope values.
/// </summary>
public interface IScopeParser
{
/// <summary>
/// Parses the requested scopes.
/// </summary>
ParsedScopesResult ParseScopeValues(IEnumerable<string> scopeValues);
}

Receives the scope values as a collection (already split from the space-delimited request parameter) and returns a ParsedScopesResult containing:

  • ParsedScopes - A collection of ParsedScopeValue objects, each with:
    • RawValue - The original scope string segment
    • ParsedName - The scope name (e.g., "transaction")
    • ParsedParameter - The extracted parameter, if any (e.g., "123")
  • Errors - Any parsing errors encountered
  • Succeeded - Whether parsing completed without errors

The DefaultScopeParser iterates over the scope values and creates a ParsedScopeValue for each. Override the virtual ParseScopeValue method to add custom parsing logic for parameterized scopes:

public class ParameterizedScopeParser : DefaultScopeParser
{
public ParameterizedScopeParser(ILogger<DefaultScopeParser> logger) : base(logger)
{ }
public override void ParseScopeValue(ParseScopeContext scopeContext)
{
// Custom parsing logic here
// Call base.ParseScopeValue(scopeContext) for standard scopes
}
}

The ParseScopeContext is a nested class within DefaultScopeParser that provides:

MemberDescription
RawValueThe original scope string being parsed
ParsedNameThe parsed scope name (read after SetParsedValues)
ParsedParameterThe parsed parameter value (read after SetParsedValues)
ErrorThe error message if parsing failed
IgnoreWhether this scope should be excluded from results
Succeededtrue if not ignored and no error
SetParsedValues(name, parameter)Sets the parsed scope name and parameter
SetIgnore()Marks this scope to be excluded from results
SetError(message)Marks this scope as invalid with an error message

Scopes like transaction:abc123 embed resource identifiers (adapt this pattern for other conventions such as document:read:456):

public override void ParseScopeValue(ParseScopeContext scopeContext)
{
const string prefix = "transaction:";
if (scopeContext.RawValue.StartsWith(prefix))
{
var parameter = scopeContext.RawValue.Substring(prefix.Length);
if (!string.IsNullOrEmpty(parameter))
{
scopeContext.SetParsedValues("transaction", parameter);
return;
}
scopeContext.SetError("transaction scope requires a parameter");
return;
}
base.ParseScopeValue(scopeContext);
}

Access the parsed parameter downstream in your IProfileService:

var txScope = context.RequestedResources.ParsedScopes
.FirstOrDefault(x => x.ParsedName == "transaction");
if (txScope?.ParsedParameter != null)
{
context.IssuedClaims.Add(new Claim("transaction_id", txScope.ParsedParameter));
}

Encode tenant context in scopes like tenant:acme:read or api.acme.read:

public override void ParseScopeValue(ParseScopeContext scopeContext)
{
// Pattern: tenant:{tenant_id}:{permission}
if (scopeContext.RawValue.StartsWith("tenant:"))
{
var parts = scopeContext.RawValue.Split(':', 3);
if (parts.Length == 3)
{
// Store as "tenant:{permission}" with tenant ID as parameter
scopeContext.SetParsedValues($"tenant:{parts[2]}", parts[1]);
return;
}
}
base.ParseScopeValue(scopeContext);
}

Reject malformed or unauthorized scope patterns early:

public override void ParseScopeValue(ParseScopeContext scopeContext)
{
// Reject scopes with invalid characters
if (scopeContext.RawValue.Contains("..") || scopeContext.RawValue.Contains("//"))
{
scopeContext.SetError("Invalid scope format");
return;
}
// Ignore internal/debug scopes in production
if (scopeContext.RawValue.StartsWith("debug:"))
{
scopeContext.SetIgnore();
return;
}
base.ParseScopeValue(scopeContext);
}

Register your custom scope parser in ConfigureServices:

builder.Services.AddIdentityServer()
.AddScopeParser<ParameterizedScopeParser>();