diff --git a/docs/en/modules/openiddict.md b/docs/en/modules/openiddict.md index c1ed40ff9f..910bb2c738 100644 --- a/docs/en/modules/openiddict.md +++ b/docs/en/modules/openiddict.md @@ -303,6 +303,18 @@ PreConfigure(options => - `UpdateAbpClaimTypes(default: true)`: Updates `AbpClaimTypes` to be compatible with the Openiddict claims. - `AddDevelopmentEncryptionAndSigningCertificate(default: true)`: Registers (and generates if necessary) a user-specific development encryption/development signing certificate. This is a certificate used for signing and encrypting the tokens and for **development environment only**. You must set it to **false** for non-development environments. +- `UseDefaultScopesForClientCredentials(default: false)`: When set to `true`, the access token issued for the `client_credentials` grant automatically grants the scopes configured on the client application (permissions prefixed with `oi_scp:`) when the client does not explicitly request any scope. +- `UseDefaultScopesForPassword(default: false)`: When set to `true`, the token response for the `password` grant automatically grants the scopes configured on the client application when the client does not explicitly request any scope. If the configured scopes include `openid`/`profile`/`email`/`roles`, the corresponding `id_token` and claim destinations are affected as well. +- `UseDefaultScopesForTokenExchange(default: false)`: When set to `true`, the token response for the `urn:ietf:params:oauth:grant-type:token-exchange` grant automatically grants the scopes configured on the client application when the client does not explicitly request any scope. If the configured scopes include `openid`/`profile`/`email`/`roles`, the corresponding `id_token` and claim destinations are affected as well. + +Example to enable the default-scope fallback for the `client_credentials` grant: + +```csharp +PreConfigure(options => +{ + options.UseDefaultScopesForClientCredentials = true; +}); +``` > `AddDevelopmentEncryptionAndSigningCertificate` cannot be used in applications deployed on IIS or Azure App Service: trying to use them on IIS or Azure App Service will result in an exception being thrown at runtime (unless the application pool is configured to load a user profile). To avoid that, consider creating self-signed certificates and storing them in the X.509 certificates store of the host machine(s). Please refer to: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html#registering-a-development-certificate diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj b/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj index 09a185919a..2bc17b4f7c 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj +++ b/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj @@ -68,6 +68,10 @@ runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native + + runtime; build; native; contentfiles; analyzers + compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native + diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs index 3a9c8109fc..48496874d0 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs @@ -25,9 +25,16 @@ public class AbpOpenIddictAspNetCoreModule : AbpModule Configure(options => { + options.ClaimsPrincipalHandlers.Add(); options.ClaimsPrincipalHandlers.Add(); }); + var preActions = context.Services.GetPreConfigureActions(); + Configure(options => + { + preActions.Configure(options); + }); + Configure(options => { options.ViewLocationFormats.Add("/Volo/Abp/OpenIddict/Views/{1}/{0}.cshtml"); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictOptions.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictOptions.cs index 3339b6d376..f0c2415fb2 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictOptions.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictOptions.cs @@ -25,4 +25,33 @@ public class AbpOpenIddictAspNetCoreOptions /// Set the url of the select account page. /// public string SelectAccountPage { get; set; } = "~/Account/SelectAccount"; + + /// + /// When set to true, the access token issued for the client_credentials grant + /// automatically includes the scopes configured on the client application (permissions + /// prefixed with oi_scp:) when the client does not explicitly request any scope. + /// Default: false. + /// + public bool UseDefaultScopesForClientCredentials { get; set; } + + /// + /// When set to true, the token response for the password grant automatically + /// grants the scopes configured on the client application (permissions prefixed with + /// oi_scp:) when the client does not explicitly request any scope. If the configured + /// scopes include openid/profile/email/roles, the corresponding + /// id_token and claim destinations are affected as well. + /// Default: false. + /// + public bool UseDefaultScopesForPassword { get; set; } + + /// + /// When set to true, the token response for the + /// urn:ietf:params:oauth:grant-type:token-exchange grant automatically grants the + /// scopes configured on the client application (permissions prefixed with oi_scp:) + /// when the client does not explicitly request any scope. If the configured scopes include + /// openid/profile/email/roles, the corresponding id_token and + /// claim destinations are affected as well. + /// Default: false. + /// + public bool UseDefaultScopesForTokenExchange { get; set; } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Claims/AbpDefaultScopesHandler.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Claims/AbpDefaultScopesHandler.cs new file mode 100644 index 0000000000..82ab4d3bb2 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Claims/AbpDefaultScopesHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.OpenIddict; + +public class AbpDefaultScopesHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency +{ + public ILogger Logger { get; set; } + = NullLogger.Instance; + + public virtual async Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context) + { + var options = context.ScopeServiceProvider + .GetRequiredService>().Value; + + var request = context.OpenIddictRequest; + if (!IsDefaultScopesEnabled(request, options)) + { + return; + } + + if (!context.Principal.GetScopes().IsDefaultOrEmpty) + { + return; + } + + var clientId = request.ClientId; + if (string.IsNullOrEmpty(clientId)) + { + return; + } + + var applicationManager = context.ScopeServiceProvider.GetRequiredService(); + var scopeManager = context.ScopeServiceProvider.GetRequiredService(); + + 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.LogDebug( + "Injecting default scopes for client {ClientId} (grant_type {GrantType}): {Scopes}", + clientId, + request.GrantType, + string.Join(", ", scopes)); + + context.Principal.SetScopes(scopes); + context.Principal.SetResources(await scopeManager.ListResourcesAsync(scopes).ToListAsync()); + } + + protected virtual bool IsDefaultScopesEnabled(OpenIddictRequest request, AbpOpenIddictAspNetCoreOptions options) + { + if (request.IsClientCredentialsGrantType()) + { + return options.UseDefaultScopesForClientCredentials; + } + + if (request.IsPasswordGrantType()) + { + return options.UseDefaultScopesForPassword; + } + + if (request.IsTokenExchangeGrantType()) + { + return options.UseDefaultScopesForTokenExchange; + } + + return false; + } +}