Consent in OpenID Connect: Balancing User Choice and Client Needs with Duende IdentityServer

Stuart Frankish |

If you've already dipped into OpenID Connect with Duende IdentityServer, you'll know that claims and scopes are the building blocks for describing user information. Previously, Khalid introduced us to claims and how Duende IdentityServer and other OpenID Connect Providers handle providing claims to clients.

Client applications can request any number of scopes, and the authorization server can decide which claims flow back to the client. It's also possible to enable consent, where the user is in control and can decide what information to share.

For example, the user can consent to share their email address or decide to omit it from the claim set. You've probably seen this in action with popular social media platforms or mobile applications.

In this article, we'll explore how Duende IdentityServer handles consent, the differences between interactive and machine-to-machine clients, how "required" vs. "optional" scopes affect the consent screen, and what happens when a client doesn't get everything it asked for.

Interactive Web Clients vs Machine-to-Machine Clients

Consent is only relevant when a human is involved. If your client is a web app that redirects a user to sign in, Duende IdentityServer can present a consent screen when that app asks for scopes beyond the bare minimum openid scope. That openid scope is always required in OpenID Connect (OIDC) because it represents the user's unique identifier.

When additional scopes are requested (such as profile, email, or custom ones), Duende IdentityServer will display them on a consent screen. This gives the user a clear list of what scopes and personal information the app is asking for. The user can then choose to approve or deny that access.

For machine-to-machine (M2M) clients, such as background services using the client credentials grant type, there's no user involved, and therefore no consent screen can and will be present. In these cases, administrators configure exactly which scopes the client can access, and Duende IdentityServer issues tokens accordingly.

new Client
{
    ClientId = "m2m.client",
    ClientSecrets = { new Secret("secret".Sha256()) },
    AllowedGrantTypes = GrantTypes.ClientCredentials,
    AllowedScopes = { "api.read", "api.write" }
};

For M2M clients, the consent is effectively baked into the configuration: the client gets only what has been explicitly allowed. Clients may request a subset of their allowed scopes, but are prevented from requesting any additional ones.

Configuring Scopes in Duende IdentityServer

Scopes in Duende IdentityServer are represented by IdentityResource (for user identity data) and ApiScope (for API permissions). You can configure each to indicate whether it's required.

  • Required scopes: must always be granted if requested, and the user cannot opt out.
  • Optional scopes: the user can uncheck them on the consent screen.

Here's a simple example:

public static IEnumerable<IdentityResource> IdentityResources =>
    new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile
        {
            Required = false, // optional – user may uncheck
            DisplayName = "Your profile data"
        },
        new IdentityResource(
            name: "employee",
            userClaims: new[] { "employee_id", "division_id" },
            displayName: "Employment Information")
        {
            Required = true // always on
        }
    };

Notice how the Required flag lives on the scope itself, not on the client. This is deliberate as it keeps scope semantics consistent across the whole system. If employee is required, it's necessary everywhere, not just for some clients.

Scopes are also always an identity provider concern rather than a client concern because they describe the categories of information or access that the provider is willing to release. In other words, the identity provider sets the rules of the exchange - what data exists, how it's grouped, and whether it's mandatory. Clients can only request from that menu, and they can't redefine the semantics.

This is a standard behaviour across identity providers including Duende IdentityServer. IdentityServer enforces this separation so that a given scope (say, employee) always means the same thing everywhere, avoiding a situation where the same scope name behaves differently depending on which client is asking. That consistency keeps consent screens trustworthy and tokens predictable.

When an interactive client requests scopes, Duende IdentityServer presents the consent screen. Required scopes are shown as checked and locked; optional ones are shown as checkboxes that the user can clear.

Here you can see the default consent screen provided by our project template:

IdentityServer consent screen

Tip: If you'd like to learn more about getting started with Duende IdentityServer and our project templates, here are a few handy links:

The consent screen itself can be toggled on or off on a per-client basis. For example, with first-party applications that you control (where the user experience should be seamless), you can disable the consent screen with RequireConsent = false;. For third-party applications or in cases where you want the user to make a deliberate decision, you can leave RequireConsent = true:

new Client
{
    ClientId = "firstparty.webapp",
    AllowedGrantTypes = GrantTypes.Code,
    RequireConsent = false, // trusted app – skip the consent screen
    AllowedScopes = { "openid", "profile", "employee" }
};

Duende IdentityServer also provides a remember consent option. If enabled, the user's decision is persisted in the consent store (by default, the operational database). This record links the user, the client and the scopes that were granted.
On future logins, Duende IdentityServer can skip showing the consent screen and automatically apply the remembered decision. Later, the user can choose to revoke consent via a management page or an admin tool, which deletes the record from the consent store, meaning IdentityServer will ask again the next time the user signs into that client.

Handling Missing or Denied Scopes

What happens if a user unchecks an optional scope on the consent screen? The client still receives tokens, but those tokens won't include the claims tied to the declined scope.

As a developer, this means you should never assume you'll get every scope you asked for. Always check what was actually granted.

For example, after login, you can check for the presence of optional scopes and toggle features on or off depending on them.

using System.Linq;
using System.Security.Claims;

// Inside a controller or Razor Page
public IActionResult Index()
{
    var user = HttpContext.User;

    // Collect all granted scopes (OIDC puts them in "scope" claims)
    var grantedScopes = user.FindAll("scope").Select(c => c.Value).ToList();

    // Example: check if "profile" scope was granted
    if (!grantedScopes.Contains("profile"))
    {
        // Disable or hide features that depend on profile information
        ViewData["ProfileEnabled"] = false;
    }
    else
    {
        ViewData["ProfileEnabled"] = true;

        // If you only need the first instance of a claim:
        var profileClaim = user.FindFirst("name");
        if (profileClaim != null)
        {
            ViewData["UserName"] = profileClaim.Value;
        }
    }

    return View();
}

Building resilient clients means gracefully degrading when optional information isn't available. This provides a far better user experience than outright breaking when a user tries to access a feature that relies on that information being available.

Consent screens are as much about user trust as they are about protocol compliance.
Here are a few practical tips:

  • Use friendly names and descriptions: Avoid dumping internal scope names like api.read. Instead, configure DisplayName and Description so the user understands what's being shared.
  • Group related claims: Don't overwhelm the user with dozens of fine-grained items; bundle them into meaningful scopes (like profile).
  • Keep it minimal: Don't ask for more than your client needs. Over-requesting leads to "consent fatigue," where users will blindly approve everything.
  • Respect consent decisions: If a user revokes consent, handle that cleanly and don't try to "game" the process by forcing them back into a corner.

These practices help keep the consent screen from becoming a barrier and instead make it a trust-building step.

Consent in OpenID Connect isn't just a checkbox on a form; it's the trust handshake between users, applications, and the identity provider.

By understanding the difference between interactive and M2M clients, configuring scopes as required or optional, handling declined scopes gracefully, and presenting well-designed consent screens, you'll deliver a Duende IdentityServer solution that's both secure and user-friendly.

And remember, consent isn't about locking down features, it's about empowering users to make informed choices. Do that well, and your apps will benefit from stronger adoption and long-term trust.