Super thin reverse proxy with YARP and Refit

Discovering technology that perfectly aligns with our needs is always exhilarating. YARP, a Microsoft creation, is one such gem, especially when it comes to managing requests through a proxy. This article delves into integrating YARP with Refit to create an efficient reverse proxy setup.

What is a Reverse Proxy?

Before diving into the specifics of YARP and Refit, it's essential to understand what a reverse proxy is and how it operates. A reverse proxy is a type of server that sits in front of web servers and forwards client requests to those web servers. Unlike a forward proxy, which acts on behalf of clients, a reverse proxy acts on behalf of the server. Its primary functions include:

  • Load Balancing: Distributing client requests across multiple servers to ensure no single server becomes overloaded, thus improving web application reliability and performance.
  • Centralized Authentication: Serving as a gateway through which all requests pass, enabling it to provide a centralized point for authentication.
  • Caching Content: Reducing server load and improving client request times by storing (caching) static and dynamic content.
  • SSL Termination: Decrypting SSL/TLS-encrypted traffic, relieving backend servers from the computational load of encryption and decryption.
  • Request Filtering and Routing: Inspecting incoming requests and routing them to the appropriate server based on various criteria.
A flowchart displaying the communication between the different components

Part 1: Reverse Proxy with YARP

Why use a Reverse Proxy?

Using a reverse proxy offers numerous benefits, particularly in terms of service and dependency management. My objective? To centralize authentication and minimize scattered secrets and logic across services. Here, 'transforms' play a pivotal role, influencing only outgoing requests. This design allows for a unique authorization strategy: leveraging OpenID Connect and client credentials for incoming requests and different authorization methods for outgoing ones, tailored to third-party authentication. You can manipulate the proxy request fully with transforms.

Configuring the Reverse Proxy with YARP

The setup begins by adding the YARP package and configuring it in either program.cs or startup.cs:

services.AddReverseProxy()
    .LoadFromConfig(Configuration.GetSection("ReverseProxy"));
    
...

app.UseEndpoints(endpoints =>
{
    endpoints.MapReverseProxy();
});

The configuration, defined in JSON, outlines routes, clusters, and crucial transforms. For example, a specific route may include an authorization policy, a default path, a catch-all path, and a transform to streamline request routing to underlying services.

{
  "ReverseProxy": {
    "Routes": {
      "default" : {
        "ClusterId": "default",
        "AuthorizationPolicy": "SystemAccess",
        "Match": {
          "Path": "default/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/default"
          }
        ]
      }
    }
  },
  "Clusters": {
    "default": {
      "Destinations": {
        "integration": {
          "Address": "https://integration.server.com"
        }
      }
    }
  }
}

Centralizing Control: A Single Point for Logging and Monitoring

One of the significant advantages of using YARP in conjunction with Refit is the ability to centralize control mechanisms, such as logging and monitoring. This centralized approach ensures consistency in how requests and responses are handled across different services.

YARP can be configured to act as a central logging point for all incoming and outgoing requests. This setup allows for uniform logging practices, making it easier to track and analyze traffic patterns, identify issues, and understand overall system performance. You can configure YARP to log details like request paths, response times, and status codes. For instance, by inserting middleware in YARP's pipeline, you can capture and log detailed information about each request:

app.UseEndpoints(endpoints =>
{
    endpoints.MapReverseProxy(proxyPipeline =>
    {
        proxyPipeline.Use(async (context, next) =>
        {
            // Logging logic before passing to the next middleware
            LogRequest(context.Request);
            await next();
            // Logging logic after the response is received
            LogResponse(context.Response);
        });
    });
});

Custom Transforms in YARP

YARP's flexibility shines in its ability to define custom transforms. Transforms refer to the modifications or manipulations applied to requests and responses as they pass through the proxy. These transforms can include altering headers, changing the request path, adding query parameters, or even modifying the body of the request. This involves implementing the ITransformProvider interface and registering the transform types. The following snippet showcases an example where we add an authorization header and a standard header to incoming requests, based on specific header values:

services.AddReverseProxy()
    .LoadFromConfig(Configuration.GetSection("ReverseProxy"))
    .AddTransforms<CustomTransformProvider>();

AddTransforms method to register a transform provider

public class CustomTransformProvider : ITransformProvider
{
    private readonly IAccessTokenForCustomClient _accessTokenForCustomClient;
    private readonly SecretOptions _secrets;

    public CustomTransformProvider(
        IAccessTokenForCustomClient accessTokenForCustomClient,
        IOptions<SecretOptions> secrets)
    {
        _accessTokenForCustomClient = accessTokenForCustomClient;
        _secrets = secrets.Value;
    }
    public void ValidateRoute(TransformRouteValidationContext context)
    {
    }

    public void ValidateCluster(TransformClusterValidationContext context)
    {
    }

    public void Apply(TransformBuilderContext context)
    {
        context.AddRequestTransform(async transformContext =>
        {
            var token = await _accessTokenForCustomClient.GetToken();
            transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
            transformContext.ProxyRequest.Headers.Add("x-app-key", _secrets.AppKey);
        });
    }
}

In this case, we can centralize authentication and prevent spreading of the logic surrounding it.

Part 2: Enhancing Client Interaction with Refit and Refitter

Refit, known for its simplicity and extensibility, is ideal for creating type-safe HTTP clients in .NET. Utilizing an Open API specification, Refitter can generate these clients, streamlining the handling of numerous query parameters. The real power lies in using Refit HTTP clients based on the Open API spec while directing their addresses through the proxy, enabling various processing mechanisms before reaching the original destination.

You generate a Refit client with Refitter by supplying a OpenAPI spec file to the CLI tool.

$ refitter [path to OpenAPI spec file] --namespace "[Your.Namespace.Of.Choice.GeneratedCode]"

Imagine we have an OpenAPI specification for a service that manages book information. With Refitter, you can automatically generate a Refit client interface for this service. Let's assume the OpenAPI spec defines endpoints for getting book details and adding a new book. The Refitter tool can generate an interface like this:

public interface IBookServiceClient
{
    [Get("/books/{id}")]
    Task<Book> GetBookAsync(int id);

    [Post("/books")]
    Task AddBookAsync(Book newBook);
}

This interface is automatically created based on the OpenAPI spec, saving you the time and effort of writing it manually. Once generated, you can use this client in your .NET application just like any other Refit client.

Adding the Refit Client to Service Collection

To integrate the Refit client into your application's dependency injection system, use the AddRefitClient method. This method not only registers the client interface but also allows you to configure the underlying HTTP client. For example, to add the IBookServiceClient generated by Refitter, you would write:

servicesCollection.AddRefitClient<IBookServiceClient>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://book.proxy/default"));

In this configuration, the base address for the HTTP client is set to the URL of the reverse proxy. This ensures that all requests made by the IBookServiceClient go through the YARP reverse proxy, benefiting from its centralized control and management features, such as logging, monitoring, and authorization.

Conclusion

The integration of YARP and Refit for reverse proxy purposes offers a streamlined, secure, and efficient way to handle service requests. By centralizing authentication and utilizing the power of transforms and type-safe clients, developers can achieve a robust and maintainable system architecture.