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:
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...
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.
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.
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:
HttpContext.GetUserAccessTokenAsync()
).In the Consumer:
IdentityModel
to get the IdP Discovery DocumentRsaSecurityKey
)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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With