Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to authenticate a user when consuming MassTransit messages in Asp.Net Core Web API?

I have several Asp.Net Core Web APIs that use Bearer authentication and IdentityServer4.AccessTokenValidation middleware to introspect tokens, authenticate the user and create claims. This works fine for HTTP requests.

I am in the process of configuring these APIs to also be MassTransit endpoints (for both Publishing and Consuming messages) using RabbitMQ as transport. I followed the instructions here for adding MassTransit to the API and for setting up message consumers. A typical workflow will be something like:

HTTP Request to API > Publish message on MassTransit > RabbitMQ > Message consumed in another API

What I'm struggling to understand is how I can create a ClaimsPrincipal when consuming messages off the bus so that I know which user to perform actions on behalf of? Where it's not an HTTP request there is no AuthenticationHandler being invoked.

What I've tried so far:

I thought I'd approach this by passing a token (and/or individual claim values) in message headers. The publish part seemed easily enough as MassTransit allows adding any number of custom headers when publishing messages using MassTransit.PublishContextExecuteExtensions.Publish. This allowed me to get messages onto the transport with information identifying a user and this info can be viewed in a consumer by manually viewing the headers e.g.

public class SomeEventConsumer : IConsumer<SomeEventData>
{
    public async Task Consume(ConsumeContext<SomeEventData> context)
    {
        var token = context.Headers["token"];
    }
} 

At this point I could take the token and call the Introspection endpoint in Identity Server manually but then I'd need to:

  1. Do this in every consumer every time and then ...
  2. ... pass that information down to logic classes etc manually instead of making use of IHttpContextAccessor.HttpContext.User.Claims or by wrapping the claims and using Dependency Injection.

To address point 1 I created a new custom middleware ...

public class AuthenticationFilter<T> : IFilter<ConsumeContext<T>> where T : class
{
    public void Probe(ProbeContext context)
    {
        var scope = context.CreateFilterScope("authenticationFilter");
    }

    public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
    {
        var token = context.Headers.Where(x => x.Key == "token").Select(x => x.Value.ToString()).Single();

        // TODO: Call token introspection

        await next.Send(context);
    }
}

public class AuthenticationFilterSpecification<T> : IPipeSpecification<ConsumeContext<T>> where T : class
{
    public void Apply(IPipeBuilder<ConsumeContext<T>> builder)
    {
        var filter = new AuthenticationFilter<T>();
        builder.AddFilter(filter);
    }

    public IEnumerable<ValidationResult> Validate()
    {
        return Enumerable.Empty<ValidationResult>();
    }
}

public class AuthenticationFilterConfigurationObserver : ConfigurationObserver, IMessageConfigurationObserver
{
    public AuthenticationFilterConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator) : base(receiveEndpointConfigurator)
    {
        Connect(this);
    }

    public void MessageConfigured<TMessage>(IConsumePipeConfigurator configurator)
        where TMessage : class
    {
        var specification = new AuthenticationFilterSpecification<TMessage>();
        configurator.AddPipeSpecification(specification);
    }
}

public static class AuthenticationExtensions
{
    public static void UseAuthenticationFilter(this IConsumePipeConfigurator configurator)
    {
        if (configurator == null)
        {
            throw new ArgumentNullException(nameof(configurator));
        }

        _ = new AuthenticationFilterConfigurationObserver(configurator);
    }
}

... and then added that into the pipeline ...

IBusControl CreateBus(IServiceProvider serviceProvider)
{
    return Bus.Factory.CreateUsingRabbitMq(cfg =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.UseAuthenticationFilter();
        // etc ...
    });
}

And this is where I'm stuck. I don't know how to authenticate the user for the scope of the request. Where it's not an HTTP request I'm not sure what best practice is here. Any suggestions or pointers would be gratefully received. Thanks...

like image 339
Gavin Sutherland Avatar asked Feb 04 '20 19:02

Gavin Sutherland


People also ask

How will you implement authentication and authorization in asp net web API?

Web API assumes that authentication happens in the host. For web-hosting, the host is IIS, which uses HTTP modules for authentication. You can configure your project to use any of the authentication modules built in to IIS or ASP.NET, or write your own HTTP module to perform custom authentication.

What is MassTransit used for?

It supports multicast, versioning, encryption, sagas, retries, transactions, distributed systems and other features. It uses a "Control Bus" design to coordinate and the Rete algorithm to route.


1 Answers

I've just been watching a Kevin Dockx course on Pluralsight that covers this scenario on Azure Service Bus, but the same principal would apply to Mass Transit or any other asynchronous communication between services using a message bus. Here's a link to the section: Securing Microservices in ASP.NET Core

Kevin's technique is to include the access token (JWT) as a property on the bus message and to then validate this in the consumer using IdentityModel.

To summarise:

In the Producer:

  1. Get the Access Token from the request (e.g. HttpContext.GetUserAccessTokenAsync()).
  2. Set this as a property in the message before sending.

In the Consumer:

  1. Use IdentityModel to get the IdP Discovery Document
  2. Extract the public signing keys from the discovery response (these must be converted to RsaSecurityKey)
  3. Call JwtSecurityTokenHandler.ValidateToken() to validate the JWT from the message. This returns a ClaimsPrincipal if successful.

If you're concerned about Access Token expiration, you can make use of the datetime that the message was enqueued as part of the token validation logic in the consumer.

Here's how the validator works (simplified):

var discoveryDocumentResponse = await httpClient.GetDiscoveryDocumentAsync("https://my.authority.com");
            
var issuerSigningKeys = new List<SecurityKey>();

foreach (var webKey in discoveryDocumentResponse.KeySet.Keys)
{
    var e = Base64Url.Decode(webKey.E);
    var n = Base64Url.Decode(webKey.N);

    var key = new RsaSecurityKey(new RSAParameters
        { Exponent = e, Modulus = n })
                {
                        KeyId = webKey.Kid
                };

    issuerSigningKeys.Add(key);
}

var tokenValidationParameters = new TokenValidationParameters()
{
        ValidAudience = "my-api-audience",
        ValidIssuer = "https://my.authority.com",
        IssuerSigningKeys = issuerSigningKeys        
};

var claimsPrincipal = new JwtSecurityTokenHandler().ValidateToken(tokenToValidate,
                    tokenValidationParameters, out var rawValidatedToken);

return claimsPrincipal;
like image 146
TallMcPaul Avatar answered Sep 22 '22 15:09

TallMcPaul