question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

AspNetCoreOperationSecurityScopeProcessor does not work for global authorization policies

See original GitHub issue

I want to use a global authorization filter, but keep the option to exclude certain controllers / methods with the [AllowAnonymous] attribute.

The generated open api document should add the the security requirement for all methods, except the ones with the [AllowAnonymous] attribute.

AspNetCoreOperationSecurityScopeProcessor works fine when using [Authorize] / [AllowAnonymous] attributes on controllers and methods, but it will not recognize global authorization policy filters:

services.AddControllers(options =>
{
    options.Filters.Add(
        new AuthorizeFilter(
            new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()));
});

or .net core 3:

app.UseEndpoints(endpoints =>
{
    endpoints
        .MapControllers()
        .RequireAuthorization();
});

Using OperationSecurityScopeProcessor instead will add the security requirement for all methods in the open api document, regardless of any additional [AllowAnonymous] attributes on controllers / methods and therefore is not really a good option.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:3
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
timvandenhofcommented, Feb 18, 2021

Working on a proof of concept, I faced the same issue as mentioned above.

Situation:

  • Policy based authentication, where each policy requires different scopes.
  • Multiple authorization flows (implicit flow + app with client id and client secret).
  • Default policy is applied, as a default for the authorize-attribute.
  • Fallback policy is applied, to force all request to be authorized conform policy.
  • Specifc endpoint requires a different policy, specified in authorize-attribute.

Not backwards compatible (so used a separate class name) and untested, but maybe the following implementation might help forward. Not sure how this will behave in a solution where also Authorize-attributes with Role are specified.

/// <summary>Generates the OAuth2 security scopes for an operation by reflecting the AuthorizeAttribute attributes.</summary>
    public class PolicyBasedOperationSecurityScopeProcessor : IOperationProcessor
    {
        private readonly string _name;

        private readonly string _fallbackPolicyName;
        private readonly string _defaultPolicyName;
        private readonly Dictionary<string, string[]> _policyScopeMapping;

        /// <summary>Initializes a new instance of the <see cref="OperationSecurityScopeProcessor"/> class with 'Bearer' name.</summary>
        public PolicyBasedOperationSecurityScopeProcessor() : this("Bearer")
        {
        }

        public PolicyBasedOperationSecurityScopeProcessor(string name) : this(name, null, null, null)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="OperationSecurityScopeProcessor"/> class.</summary>
        /// <param name="name">The security definition name.</param>
        /// <param name="policyScopeMapping">Mapping between policy name and required scopes.</param>
        /// <param name="fallbackPolicyName">Fallback policy name, if any.</param>
        /// <param name="defaultPolicyName">Default policy name, if any.</param>
        public PolicyBasedOperationSecurityScopeProcessor(string name, Dictionary<string, string[]> policyScopeMapping, string fallbackPolicyName, string defaultPolicyName)
        {
            _name = name;
            _fallbackPolicyName = fallbackPolicyName;
            _defaultPolicyName = defaultPolicyName;
            _policyScopeMapping = policyScopeMapping;
        }

        /// <summary>Processes the specified method information.</summary>
        /// <param name="context"></param>
        /// <returns>true if the operation should be added to the Swagger specification.</returns>
        public bool Process(OperationProcessorContext context)
        {
            var aspNetCoreContext = (AspNetCoreOperationProcessorContext)context;

            var endpointMetadata = aspNetCoreContext?.ApiDescription?.ActionDescriptor?.TryGetPropertyValue<IList<object>>("EndpointMetadata");
            if (endpointMetadata != null)
            {
                var allowAnonymous = endpointMetadata.OfType<AllowAnonymousAttribute>().Any();
                if (allowAnonymous)
                {
                    // Anonymous access allowed, no security requirements needed.
                    return true;
                }

                var authorizeAttributes = endpointMetadata.OfType<AuthorizeAttribute>().ToList();
                if (!authorizeAttributes.Any())
                {
                    if (string.IsNullOrEmpty(_fallbackPolicyName))
                    {
                        // No default policy and no authorization atribute, so allow anonymous access.
                        return true;
                    }
                    else
                    {
                        var requiredScopes = GetScopesForPolicy(_fallbackPolicyName);
                        if (!requiredScopes.Any())
                        {
                            // Fallback policy applies, but no scopes required, so allow anonymous access.
                            return true;
                        }
                        else
                        {
                            // Falback policy applies, so require scopes.
                            ApplyPolicyScopes(context.OperationDescription.Operation, requiredScopes);
                        }
                    }
                }
                else
                {
                    // There is one or more authorize attributes.
                    // If multiple policies are defined, those should be stacked.
                    // If no policy is specified, the default policy applies.
                    // Otherwise the policy specified in the attribute applies.
                    var applyingPolicies = GetPolicyNamesScopes(authorizeAttributes);
                    var requiredScopes = applyingPolicies.SelectMany(policyName => GetScopesForPolicy(policyName)).Distinct();
                    ApplyPolicyScopes(context.OperationDescription.Operation, requiredScopes);
                }
            }

            return true;
        }

        private void ApplyPolicyScopes(OpenApiOperation operation, IEnumerable<string> requiredScopes)
        {
            if (operation.Security == null)
            {
                operation.Security = new List<OpenApiSecurityRequirement>();
            }
            
            operation.Security.Add(new OpenApiSecurityRequirement
            {
                { _name, requiredScopes }
            });        
        }

        private IEnumerable<string> GetPolicyNamesScopes(IEnumerable<AuthorizeAttribute> authorizeAttributes)
        {
            return authorizeAttributes
                .Select(a => a.Policy ?? _defaultPolicyName) // Apply default policy if not specified in attribute.
                .Distinct();
        }

        private string[] GetScopesForPolicy(string policyName)
        {
            var scopes = Array.Empty<string>();
            _policyScopeMapping?.TryGetValue(policyName, out scopes);

            return scopes;
        }
    }
0reactions
RicoSutercommented, Feb 15, 2021

If you look at the code then you see that only roles are taken into account: https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Generation.AspNetCore/Processors/AspNetCoreOperationSecurityScopeProcessor.cs

Can you create a PR to improve that with the right behavior (and hopefully not breaking changes)?

Read more comments on GitHub >

github_iconTop Results From Across the Web

NSwag's AspNetCoreOperationSecurityScopeProcessor ...
I've decorated actions in my controllers with [AllowAnonymous] and [Authorize(AuthenticationSchemes = "ClientApp")] , however NSwag marks all of ...
Read more >
Setting global authorization policies using the ...
In this post I show multiple ways to configure global authorization policies, and look at the difference between the DefaultPolicy and the ...
Read more >
Policy-based authorization in ASP.NET Core
Learn how to create and use authorization policy handlers for enforcing authorization requirements in an ASP.NET Core app.
Read more >
Custom Authorization Policy Providers in ASP.NET Core
Learn how to use a custom IAuthorizationPolicyProvider in an ASP.NET Core app to dynamically generate authorization policies.
Read more >
Enabling multiple authorization methods in NSwag : r/dotnet
Hey, I'm looking for a way to enable both JWT Bearer authorization and a simple API-KEY method in the nswag UI.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found