Open Closed

How to return a default scope when requesting an access token? #10635


User avatar
0
geertveenstra created

Hello, we have a client that needs to access our API and is requesting first an Access Token through OpenIddict (grant_type=client_credentials and client_id / secret). But the client is not able to request a specific scope (which to my understanding would be needed to secure the API with the [Authorize] attribute). Is it possible to return a defined scope in the access_token in the response? This is the current setup of the application for the access token:

The Authorize part is working when the selected scope is requested in the token request.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

3 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    OpenIddict (and ABP's TokenController.HandleClientCredentialsAsync) only puts the scopes the client explicitly asked for into the access token. The scopes you tick on the application page are an allow-list, not defaults — if the client sends no scope parameter, the issued token has no scope at all, which is why your [Authorize] check fails.

    OAuth 2.0 actually allows the authorization server to fall back to a pre-defined default scope (RFC 6749 §3.3 and §4.4.2), but ABP doesn't ship a built-in switch for that. The cleanest way to add it is a small IAbpOpenIddictClaimsPrincipalHandler that, on client_credentials requests with an empty scope, copies the application's oi_scp: permissions into the principal:

    using System.Collections.Immutable;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Logging.Abstractions;
    using OpenIddict.Abstractions;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.OpenIddict;
    
    public class DefaultClientCredentialsScopeHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency
    {
        public ILogger<DefaultClientCredentialsScopeHandler> Logger { get; set; }
            = NullLogger<DefaultClientCredentialsScopeHandler>.Instance;
    
        public async Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
        {
            var request = context.OpenIddictRequest;
            if (!string.Equals(request.GrantType, OpenIddictConstants.GrantTypes.ClientCredentials, StringComparison.Ordinal))
            {
                return;
            }
    
            if (!context.Principal.GetScopes().IsDefaultOrEmpty)
            {
                return; // client requested scopes explicitly, leave it alone
            }
    
            var clientId = request.ClientId;
            if (string.IsNullOrEmpty(clientId))
            {
                return;
            }
    
            var applicationManager = context.ScopeServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
            var scopeManager = context.ScopeServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
    
            var application = await applicationManager.FindByClientIdAsync(clientId);
            if (application == null)
            {
                return;
            }
    
            var permissions = await applicationManager.GetPermissionsAsync(application);
            var prefix = OpenIddictConstants.Permissions.Prefixes.Scope;
    
            var scopes = permissions
                .Where(p => p.StartsWith(prefix, StringComparison.Ordinal))
                .Select(p => p[prefix.Length..])
                .ToImmutableArray();
    
            if (scopes.IsDefaultOrEmpty)
            {
                return;
            }
    
            Logger.LogInformation("Injecting configured scopes for {ClientId}: {Scopes}", clientId, string.Join(", ", scopes));
    
            context.Principal.SetScopes(scopes);
    
            var resources = new List<string>();
            await foreach (var resource in scopeManager.ListResourcesAsync(scopes))
            {
                resources.Add(resource);
            }
            context.Principal.SetResources(resources);
        }
    }
    

    Register it in your module's ConfigureServices:

    Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
    {
        options.ClaimsPrincipalHandlers.Add<DefaultClientCredentialsScopeHandler>();
    });
    

    One thing to watch out for: register it with Configure, not PreConfigure. PreConfigure runs before the framework's own Configure<AbpOpenIddictClaimsPrincipalOptions> block in AbpOpenIddictAspNetCoreModule, and you'll find the handler gets dropped from the final list. I verified the whole flow locally on rel-10.3 against your entervo_infinite-style client (client_credentials + oi_scp:ConnectorNL permission):

    • Request without scope → token comes back with "scope": "ConnectorNL" and "aud": "ConnectorNLResource" (handler kicks in).
    • Request with scope=ConnectorNL → identical token (handler is a no-op).

    We'll also look into making this a first-class option on the OpenIddict module in a future release, so you don't have to maintain the handler yourself.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    geertveenstra created

    Hi,

    Thank you very much, exactly what I needed!

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Great

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 16, 2026, 14:50
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.