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.
3 Answer(s)
-
0
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 noscopeparameter, 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
IAbpOpenIddictClaimsPrincipalHandlerthat, onclient_credentialsrequests with an empty scope, copies the application'soi_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, notPreConfigure.PreConfigureruns before the framework's ownConfigure<AbpOpenIddictClaimsPrincipalOptions>block inAbpOpenIddictAspNetCoreModule, and you'll find the handler gets dropped from the final list. I verified the whole flow locally onrel-10.3against yourentervo_infinite-style client (client_credentials+oi_scp:ConnectorNLpermission):- 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) - Request without
-
0
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)
