Table of Contents

Parameterized scopes and redirect URIs

New in 0.116.0, Duende's IScopeParser and IRedirectUriValidator. The code on this page is included from samples/Protocols.Resources, which a test boots: it is granted transaction:123, refused transaction:, and sends the native client's authorization request to a loopback port of its choosing.

var identityServer = builder.Services
    .AddIdentityServer(options => options.KeyManagement.Enabled = false)

    // Scope values with a parameter: transaction:123.
    .AddScopeParser<TransactionScopes>()

    // A native client that registered http://127.0.0.1 may use that address on any port (RFC 8252 §7.3).
    .AddAppAuthRedirectUriValidator();

A scope with a parameter

transaction:123 can ask for the transaction scope about transaction 123. Derive from DefaultScopeParser and say what each value names:

/// <summary>
/// <c>transaction:{id}</c> asks for the <c>transaction</c> scope about one transaction. The client and the resource
/// store are checked against the name; the value the client sent is what is granted and carried in the token.
/// </summary>
public sealed class TransactionScopes(ILogger<DefaultScopeParser> logger) : DefaultScopeParser(logger)
{
    private const string Prefix = "transaction:";

    /// <inheritdoc />
    public override void ParseScopeValue(ParseScopeContext scopeContext)
    {
        ArgumentNullException.ThrowIfNull(scopeContext);

        if (scopeContext.RawValue.StartsWith(Prefix, StringComparison.Ordinal))
        {
            var id = scopeContext.RawValue[Prefix.Length..];
            if (id.Length == 0)
            {
                scopeContext.SetError("A transaction needs an id.");   // invalid_scope
            }
            else
            {
                scopeContext.SetParsedValues("transaction", id);
            }
        }

        // Anything else is left as it is: a plain scope, its own name.
    }
}

The client and the resource store are checked against the name — the client registers transaction — and the value the client sent is what is granted, consented to and carried in the tokens' scope: ResourceValidationResult.ParsedScopes holds each value with its name and parameter, and RawScopeValues (which was GrantedScopes) the values. A value the parser refuses is an invalid_scope; SetIgnore() drops one from the request. The consent and device pages show a parameterized scope by its display name and parameter, and post back the value.

Consent, as Duende decides it. A parameterized scope is asked about every time, whatever was remembered — a decision about one transaction is not one about the next — and so, since 0.116.0, is offline_access. A request for no scopes is not asked about. IConsentService.RequiresConsentAsync takes ParsedScopeValues, Duende's type, where it took strings.

An access token narrowed to one API (resource, RFC 8707) carries that API's scopes and offline_access in its scope, and no longer the identity scopes, as Duende's FilterByResourceIndicator keeps them; see Resource indicators.

Which redirect URIs a client may use

IRedirectUriValidator decides redirect_uri at the authorize endpoint and on the interactive pages, and post_logout_redirect_uri at end session. The default, StrictRedirectUriValidator, accepts exactly what the client registered — character for character, as RFC 9700 §2.1 requires, where Duende's ignores case.

For native clients (RFC 8252 §7.3), AddAppAuthRedirectUriValidator lets a client that requires PKCE and registered http://127.0.0.1 use that address on any port:

// A native application. Each setting shown is the default, written out: PKCE required and with S256 only,
// and a refresh token that is good for one use. The loopback address is registered without a port.
new Client
{
    ClientId = "field-notes",
    RequireClientSecret = false,
    AllowedGrantTypes = { OidcConstants.GrantTypes.AuthorizationCode },
    RedirectUris = { "http://127.0.0.1" },
    AllowedScopes = { "openid", "profile", "ledger.read", "orders.read" },
    AllowOfflineAccess = true,

    RequirePkce = true,
    AllowPlainTextPkce = false,
    RefreshTokenUsage = IdentityServerConstants.TokenUsage.OneTimeOnly,
},

A host's own validator derives from StrictRedirectUriValidator and registers with AddRedirectUriValidator. Whatever it accepts, ValidationOptions.InvalidRedirectUriPrefixes still refuses (see ValidationOptions).