azure-active-directoryazure-ad-msalmicrosoft-identity-webms-yarp

How to validate the audience in the YARP routes?


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.


Solution

  • 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.

    1. Set Up Azure AD with Multiple Apps

    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.

    2. We Implemented JWT Validation in YARP using C# and .NET

    1. 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.