JasperFx / wolverine

Supercharged .NET server side development!
https://wolverinefx.net
MIT License
1.17k stars 128 forks source link

How to do authentication middleware? #220

Open jeremydmiller opened 1 year ago

jeremydmiller commented 1 year ago

From https://github.com/JasperFx/wolverine/issues/173

Hi @jeremydmiller,

at the risk of flooding you with feedback (take your time!), I today tried to Wolverine-ify RBAC on the granularity of a command (still talking about IMessageBus).

I use KeyCloak for authentication and there is an excellent NuGet with an example to implement RBAC with a Mediatr behavior..

The code has a comment // TODO: consider reflection performance impact which I'm also after.

From my understanding, the Wolverine equivalent of IPipelineBehavior would be a middleware that would incur the same reflection cost per processed message. My idea was to make use of the code generation capability around the handler and pay that cost only once during setup.


using System.Runtime.CompilerServices;

using Application.Authorization;

using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;

using Lamar;

using Microsoft.Extensions.Logging;

using Wolverine.Configuration;
using Wolverine.Runtime.Handlers;

namespace Infrastructure.Authorization;

public class AuthorizationPolicy : IHandlerPolicy
{
  static readonly Dictionary<Type, string[]> MessageTypeToPolicies = new();

  public void Apply(HandlerGraph graph, GenerationRules rules, IContainer container)
  {
    foreach (var chain in graph.Chains)
    {
      Apply(chain);
    }
  }

  void Apply(HandlerChain chain)
  {
    var policies = chain.MessageType
                        .GetCustomAttributes(typeof(AuthorizeAttribute), true)
                        .Cast<AuthorizeAttribute>()
                        .Select(x => x.Policy)
                        .Where(x => !string.IsNullOrWhiteSpace(x))
                        .ToArray();

    if (!policies.Any())
    {
      return;
    }

    MessageTypeToPolicies.Add(chain.MessageType, policies!);

    var method = GetType()
                 .GetMethod(nameof(EnforcePolicy))
                 .MakeGenericMethod(chain.MessageType);

    var methodCall = new MethodCall(GetType(), method);

    chain.Middleware.Add(methodCall);
  }

  [MethodImpl(MethodImplOptions.AggressiveInlining)]
  public static async Task EnforcePolicy<T>(IIdentityService identityService, ILogger logger, T message)
  {
    var policies = MessageTypeToPolicies[message.GetType()];

    foreach (var policy in policies)
    {
      if (await identityService.AuthorizeAsync(policy))
      {
        continue;
      }

      logger.LogWarning("Failed {Policy} policy authorization with user {User} for {Message}",
                        policy,
                        identityService.UserId,
                        message);

      throw new UnauthorizedAccessException();
    }
  }
}

The Apply does the reflection bits and then injects a method call, like a middleware's Before(). But the Before() variant would always need to reflect, right?

As you can see the handler policy uses a dictionary to cache the roles per message, but my ultimate goal would be to pass the list of roles directly to EnforcePolicy, perhaps as the first argument.

I tried that for a couple of hours, even cloned JasperFx.CodeGeneration, but I could not find a way to make the first argument a constant expression. Perhaps another way is to register "required roles per message" in the container, but that seems a bit too overboard. It would be great if you could share your ideas!

jeremydmiller commented 1 year ago

@agross I'll have to give this one a better look and get back to you. It's exactly the kind of thing that I think Wolverine should do much more efficiently than MediatR et al.