We are working on a system where we have YARP as a gateway and several APIs for different data domains. The APIs are protected with Azure AD. Using MSAL (Microsoft.Identity.Web) is easy as there are many examples of how to protect APIs or Web Apps. The APIs are called from different types of clients (SPA, CLI apps, web apps, etc...), and with different flows. Now, one of the requirements is that YARP works as a first line of defense, and for this we want YARP to validate the JWTs that are sent through each of the protected routes, that is, we want to authenticate and authorize each call. Although the authority (IdP) for all the APIs is the same, Azure AD, not all of them are registered in the same tenant and of course, the client-Id (audience) is different for each API. Has anyone had to implement something similar? Note: We don't want to validate specific scopes per route and in terms of authorization it is enough to validate that the user is authenticated.
The scenario you're describing – where a reverse proxy or gateway is responsible for validating JWT tokens before forwarding requests to various microservices or APIs – is not uncommon. YARP is designed to be highly customizable, and with .NET's middleware pipeline, you can integrate JWT validation.
I am describing below an approach we had used in one of the projects.
We had multiple Azure AD Apps, So we had different issuer
and audience
values for JWT validation based on these Azure AD apps. Which is fine.
Middleware to Validate JWT:
In the YARP pipeline, we injected a middleware to inspect the Authorization
header of the incoming request and validate the JWT token.
Something like below
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
// Custom JWT validation middleware
app.Use(async (context, next) =>
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (string.IsNullOrEmpty(token))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Authorization header missing.");
return;
}
try
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(token);
// Determine the API (audience) based on the request, and set issuer and audience values accordingly
var validIssuer = "<ISSUER_FROM_YOUR_AZURE_AD>";
var validAudience = jwtSecurityToken.Audiences.FirstOrDefault(); // or set based on the API
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = validIssuer,
ValidAudience = validAudience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("<YOUR_SECRET_KEY>")), // Normally, you'd fetch this dynamically from Azure AD's jwks endpoint.
ValidateLifetime = true // This checks the expiry
};
// This will throw if invalid
jwtSecurityTokenHandler.ValidateToken(token, validationParameters, out _);
}
catch
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid token.");
return;
}
await next.Invoke();
});
app.UseEndpoints(endpoints =>
{
endpoints.MapReverseProxy();
});
}
We used values for the issuer, audience, and signing key as hard coded in above. In a your scenario, you may need a more dynamic approach where these values change based on which API the request is targeting. This could involve maintaining a configuration or map of API paths to their corresponding Azure AD settings, and fetching them dynamically in the middleware.