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.
When to Use
Section titled “When to Use”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
Interface
Section titled “Interface”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);}ParseScopeValues
Section titled “ParseScopeValues”Receives the scope values as a collection (already split from the space-delimited request parameter) and returns a ParsedScopesResult containing:
ParsedScopes- A collection ofParsedScopeValueobjects, each with:RawValue- The original scope string segmentParsedName- The scope name (e.g.,"transaction")ParsedParameter- The extracted parameter, if any (e.g.,"123")
Errors- Any parsing errors encounteredSucceeded- Whether parsing completed without errors
Default Implementation
Section titled “Default Implementation”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 }}DefaultScopeParser.ParseScopeContext
Section titled “DefaultScopeParser.ParseScopeContext”The ParseScopeContext is a nested class within DefaultScopeParser that provides:
| Member | Description |
|---|---|
RawValue | The original scope string being parsed |
ParsedName | The parsed scope name (read after SetParsedValues) |
ParsedParameter | The parsed parameter value (read after SetParsedValues) |
Error | The error message if parsing failed |
Ignore | Whether this scope should be excluded from results |
Succeeded | true 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 |
Common Scenarios
Section titled “Common Scenarios”Transaction or Resource IDs
Section titled “Transaction or Resource IDs”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));}Multi-Tenant Scopes
Section titled “Multi-Tenant Scopes”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);}Dynamic Scope Validation
Section titled “Dynamic Scope Validation”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);}Registration
Section titled “Registration”Register your custom scope parser in ConfigureServices:
builder.Services.AddIdentityServer() .AddScopeParser<ParameterizedScopeParser>();