title: Authorization middleware doesn't recognize policies added through MVC conventions
body: <p>This is an ASP.NET Core 3.1 web application. I'm adding policies to my <code>ApiController</code> read/write methods using code adapted from <a href="https://dejanstojanovic.net/aspnet/2019/april/why-you-should-not-use-authorize-attributes-to-protect-your-api-endpoints/" rel="nofollow noreferrer">this blog post</a>:</p>
  <pre><code>public class DefaultApiAuthorizationConvention : IActionModelConvention
  {
      private readonly string _apiReadPolicyName;
      private readonly string _apiWritePolicyName;
  
      public DefaultApiAuthorizationConvention(string apiReadPolicyName = Constants.POLICY_API_READ, string apiWritePolicyName = Constants.POLICY_API_WRITE)
      {
          _apiReadPolicyName = apiReadPolicyName;
          _apiWritePolicyName = apiWritePolicyName;
      }
  
      public void Apply(ActionModel action)
      {
          // only apply to API controllers
          if (!action.Controller.Attributes.Any(a =&gt; a is ApiControllerAttribute))
              return;
  
          // Require API Write policy for POST/PUT/DELETE methods, and API Read policy for GET methods
          if (action.Attributes.Any(a =&gt; a is HttpPostAttribute || a is HttpPutAttribute || a is HttpDeleteAttribute))
          {
              action.Filters.Add(new AuthorizeFilter(policy: _apiWritePolicyName));
          }
          else if (action.Attributes.Any(a =&gt; a is HttpGetAttribute))
          {
              action.Filters.Add(new AuthorizeFilter(policy: _apiReadPolicyName));
          }
      }
  }
  </code></pre>
  <p>The convention is added and the policies are registered in <code>Startup.cs</code>, and that's all fine and dandy. I also have default and fallback policies defined.</p>
  <pre><code>services.AddMvc(options =&gt;
  {
      options.Conventions.Add(new AppAuth.DefaultApiAuthorizationConvention());
  });
  services.AddAuthorization(options =&gt;
  {
      options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
          .RequireAuthenticatedUser()
          .RequireRole(WebConstants.ROLE_SITE_ADMIN, WebConstants.ROLE_SITE_BROWSER)
          .Build();
      // general API policy
      options.AddPolicy(AppAuth.Constants.POLICY_API_READ, policy =&gt; 
          policy.RequireAuthenticatedUser()
                .RequireRole(WebConstants.ROLE_SITE_ADMIN, WebConstants.ROLE_API_READ));
      options.AddPolicy(AppAuth.Constants.POLICY_API_WRITE, policy =&gt; 
          policy.RequireAuthenticatedUser()
                .RequireRole(WebConstants.ROLE_SITE_ADMIN, WebConstants.ROLE_API_WRITE));
      // other policies omitted
  });
  </code></pre>
  <p>The controller is like so, with no <code>[Authorize]</code> attribute on either the controller or method.</p>
  <pre><code>[ApiController]
  [Route(&quot;api/[controller]&quot;)]
  public class ValuesController : ControllerBase
  {
      [HttpGet]
      public async Task&lt;IActionResult&gt; Get()
      {
          return Ok(await _service.Get());
      }
  }
  </code></pre>
  <p>The problem is that when a user who is a member of <code>ROLE_API_READ</code> as per the policy calls the API method, the <a href="https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs" rel="nofollow noreferrer">AuthorizationMiddleware</a> always marks them as unauthorized.</p>
  <p>Investigating the source code of the middleware, we find:</p>
  <pre><code>var authorizeData = endpoint?.Metadata.GetOrderedMetadata&lt;IAuthorizeData&gt;() ?? Array.Empty&lt;IAuthorizeData&gt;();
  </code></pre>
  <p>I made a copy of the class in my project so I could debug, and when I put a break point here, <code>authorizeData</code> turned out to be a an empty array. So then we check the <a href="https://github.com/dotnet/aspnetcore/blob/86ea8e019eb315dd6b68bd7fcc796a0fcb6a67c9/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs#L101" rel="nofollow noreferrer">EndpointMetadataCollection</a> class:</p>
  <pre><code>public IReadOnlyList&lt;T&gt; GetOrderedMetadata&lt;T&gt;() where T : class
  {
      if (_cache.TryGetValue(typeof(T), out var result))
      {
          return (T[])result;
      }
      return GetOrderedMetadataSlow&lt;T&gt;(); // private method that gets the metadata real-time
  }
  </code></pre>
  <p>This <code>_cache</code> variable is set in the constructor and contains the empty array of <code>IAuthorizeData</code>:
  <a href="https://i.stack.imgur.com/ErdD1.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/ErdD1.png" alt="No IAuthorizeData in the Metadata class" /></a></p>
  <p>The <code>Metadata</code> object itself contains the <code>AuthorizeFilter</code> as added by the convention:
  <a href="https://i.stack.imgur.com/n2wWC.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/n2wWC.png" alt="enter image description here" /></a></p>
  <p>I did contact the blog author, who surprisingly doesn't appear to be an Stack Overflow, but I wanted to open it to the wider community to see what my options were.</p>
  <p>Thanks in advance for your help.</p>