Creating a proxy to another web api with Asp.net core

88,859

Solution 1

I ended up implementing a proxy middleware inspired by a project in Asp.Net's GitHub.

It basically implements a middleware that reads the request received, creates a copy from it and sends it back to a configured service, reads the response from the service and sends it back to the caller.

Solution 2

If anyone is interested, I took the Microsoft.AspNetCore.Proxy code and made it a little better with middleware.

Check it out here: https://github.com/twitchax/AspNetCore.Proxy. NuGet here: https://www.nuget.org/packages/AspNetCore.Proxy/. Microsoft archived the other one mentioned in this post, and I plan on responding to any issues on this project.

Basically, it makes reverse proxying another web server a lot easier by allowing you to use attributes on methods that take a route with args and compute the proxied address.

[ProxyRoute("api/searchgoogle/{query}")]
public static Task<string> SearchGoogleProxy(string query)
{
    // Get the proxied address.
    return Task.FromResult($"https://www.google.com/search?q={query}");
}

Solution 3

This post talks about writing a simple HTTP proxy logic in C# or ASP.NET Core. And allowing your project to proxy the request to any other URL. It is not about deploying a proxy server for your ASP.NET Core project.

Add the following code anywhere of your project.

        public static HttpRequestMessage CreateProxyHttpRequest(this HttpContext context, Uri uri)
        {
            var request = context.Request;

            var requestMessage = new HttpRequestMessage();
            var requestMethod = request.Method;
            if (!HttpMethods.IsGet(requestMethod) &&
                !HttpMethods.IsHead(requestMethod) &&
                !HttpMethods.IsDelete(requestMethod) &&
                !HttpMethods.IsTrace(requestMethod))
            {
                var streamContent = new StreamContent(request.Body);
                requestMessage.Content = streamContent;
            }

            // Copy the request headers
            foreach (var header in request.Headers)
            {
                if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
                {
                    requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                }
            }

            requestMessage.Headers.Host = uri.Authority;
            requestMessage.RequestUri = uri;
            requestMessage.Method = new HttpMethod(request.Method);

            return requestMessage;
        }

This method covert user sends HttpContext.Request to a reusable HttpRequestMessage. So you can send this message to the target server.

After your target server response, you need to copy the responded HttpResponseMessage to the HttpContext.Response so the user's browser just gets it.

        public static async Task CopyProxyHttpResponse(this HttpContext context, HttpResponseMessage responseMessage)
        {
            if (responseMessage == null)
            {
                throw new ArgumentNullException(nameof(responseMessage));
            }

            var response = context.Response;

            response.StatusCode = (int)responseMessage.StatusCode;
            foreach (var header in responseMessage.Headers)
            {
                response.Headers[header.Key] = header.Value.ToArray();
            }

            foreach (var header in responseMessage.Content.Headers)
            {
                response.Headers[header.Key] = header.Value.ToArray();
            }

            // SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response.
            response.Headers.Remove("transfer-encoding");

            using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
            {
                await responseStream.CopyToAsync(response.Body, _streamCopyBufferSize, context.RequestAborted);
            }
        }

And now the preparation is complete. Back to our controller:

    private readonly HttpClient _client;

    public YourController()
    {
        _client = new HttpClient(new HttpClientHandler()
        {
            AllowAutoRedirect = false
        });
    }

        public async Task<IActionResult> Rewrite()
        {
            var request = HttpContext.CreateProxyHttpRequest(new Uri("https://www.google.com"));
            var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
            await HttpContext.CopyProxyHttpResponse(response);
            return Ok();
        }

And try to access it. It will be proxied to google.com

![](/uploads/img-f2dd7ca2-79e4-4846-a7d0-6685f9b33ff4.png)

Solution 4

A nice reverse proxy middleware implementation can also be found here: https://auth0.com/blog/building-a-reverse-proxy-in-dot-net-core/

Note that I replaced this line here

requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());

with

requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToString());

Original headers (e.g. like an authorization header with a bearer token) would not be added without my modification in my case.

Solution 5

Piggy-backing on James Lawruk's answer https://stackoverflow.com/a/54149906/6596451 to get the twitchax Proxy attribute to work, I was also getting a 404 error until I specified the full route in the ProxyRoute attribute. I had my static route in a separate controller and the relative path from Controller's route was not working.

This worked:

public class ProxyController : Controller
{
    [ProxyRoute("api/Proxy/{name}")]
    public static Task<string> Get(string name)
    {
        return Task.FromResult($"http://www.google.com/");
    }
}

This does not:

[Route("api/[controller]")]
public class ProxyController : Controller
{
    [ProxyRoute("{name}")]
    public static Task<string> Get(string name)
    {
        return Task.FromResult($"http://www.google.com/");
    }
}

Hope this helps someone!

Share:
88,859
Gimly
Author by

Gimly

Software developer working mainly on .Net.

Updated on July 05, 2022

Comments

  • Gimly
    Gimly almost 2 years

    I'm developing an ASP.Net Core web application where I need to create a kind of "authentication proxy" to another (external) web service.

    What I mean by authentication proxy is that I will receive requests through a specific path of my web app and will have to check the headers of those requests for an authentication token that I'll have issued earlier, and then redirect all the requests with the same request string / content to an external web API which my app will authenticate with through HTTP Basic auth.

    Here's the whole process in pseudo-code

    • Client requests a token by making a POST to a unique URL that I sent him earlier
    • My app sends him a unique token in response to this POST
    • Client makes a GET request to a specific URL of my app, say /extapi and adds the auth-token in the HTTP header
    • My app gets the request, checks that the auth-token is present and valid
    • My app does the same request to the external web API and authenticates the request using BASIC authentication
    • My app receives the result from the request and sends it back to the client

    Here's what I have for now. It seems to be working fine, but I'm wondering if it's really the way this should be done or if there isn't a more elegant or better solution to this? Could that solution create issues in the long run for scaling the application?

    [HttpGet]
    public async Task GetStatement()
    {
        //TODO check for token presence and reject if issue
    
        var queryString = Request.QueryString;
        var response = await _httpClient.GetAsync(queryString.Value);
        var content = await response.Content.ReadAsStringAsync();
    
        Response.StatusCode = (int)response.StatusCode;
        Response.ContentType = response.Content.Headers.ContentType.ToString();
        Response.ContentLength = response.Content.Headers.ContentLength;
    
        await Response.WriteAsync(content);
    }
    
    [HttpPost]
    public async Task PostStatement()
    {
        using (var streamContent = new StreamContent(Request.Body))
        {
            //TODO check for token presence and reject if issue
    
            var response = await _httpClient.PostAsync(string.Empty, streamContent);
            var content = await response.Content.ReadAsStringAsync();
    
            Response.StatusCode = (int)response.StatusCode;
    
            Response.ContentType = response.Content.Headers.ContentType?.ToString();
            Response.ContentLength = response.Content.Headers.ContentLength;
    
            await Response.WriteAsync(content);
        }
    }
    

    _httpClient being a HttpClient class instantiated somewhere else and being a singleton and with a BaseAddressof http://someexternalapp.com/api/

    Also, is there a simpler approach for the token creation / token check than doing it manually?

  • Dmitriy
    Dmitriy over 6 years
    could you share your implements of your middleware? If it possible. Is it strongly based on .Net Core? Thanks.
  • Gimly
    Gimly over 6 years
    @Dmitriy No, I'm sorry I cannot share the implementation as it is part of a closed source program. But it's basically the same code as in the question implemented as a middleware. Check the github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Pr‌​oxy/… file to get an idea on how to start the middleware.
  • Kugel
    Kugel over 6 years
    Can you update the nuget package this code does not work with published 0.2.0
  • Allan
    Allan over 6 years
    Not sure if I am missing something with this code or not but I cannot resolve services.AddProxy(...). I am using Microsoft.AspNetCore.Proxy v0.2.0. Also, the RunProxy method does not accept a Uri as a parameter. What version was used for this example?
  • Kerem Demirer
    Kerem Demirer over 6 years
    I used v0.2 nuget from preview feeds: dotnet.myget.org/feed/aspnetcore-release/package/nuget/…
  • Bangyou
    Bangyou about 6 years
    I have the same problem with @Allan. Is any solution for it?
  • Bangyou
    Bangyou about 6 years
    It seems the SDK not support asp net core 2.1 when I compile the source codes.
  • amin89
    amin89 about 6 years
    Same thing.cannot resolve services.AddProxy() function with Microsoft.AspNetCore.Proxy v0.2.0 ... Since March 2018, can anyobody give us the solution??
  • JPelletier
    JPelletier almost 6 years
    @amin89 The method name was changed to RunProxy. It's not clear if the feature is experimental or not github.com/aspnet/Home/issues/2931
  • amin89
    amin89 almost 6 years
    @Allan I found it if u still need the solution.
  • Jsandesu
    Jsandesu over 5 years
    Thanks. Couldn't get the ProxyRoute attribute to work. Got a 404. Probably something I was doing wrong. Had success using the UseProxy() method, so thanks again.
  • twitchax
    twitchax over 5 years
    Ahhh, nice. Feel free to file a bug on that. Should be an easy fix.
  • twitchax
    twitchax over 5 years
    Check out the solution below. The middleware does not take the class route into account yet. Feel free to file an issue! :)
  • BrunoMartinsPro
    BrunoMartinsPro over 4 years
    Was this the only configuration you made?
  • BrunoMartinsPro
    BrunoMartinsPro over 4 years
    never mind i changed the "api/someexternalapp-proxy/{arg1}" to "api/someexternalapp-proxy/{**catchall}" and added in ConfigureServices "services.AddProxies();" and its now working!
  • Eddie
    Eddie over 4 years
    This proxy also doesn't add the query string to requests that it proxies. I added that in BuildTargetUri using string query = request.QueryString.ToString();
  • genuinefafa
    genuinefafa over 3 years
    oh @spencer741, this is pretty interesting and seems too easy too set up
  • spencer741
    spencer741 over 3 years
    @genuinefafa Yes indeed... is definitely in early stages. Wouldn't trust from a security or performance perspective just yet.
  • spencer741
    spencer741 about 3 years
    Looks like it is very promising now.
  • Renato Sanhueza
    Renato Sanhueza about 3 years
    The problem with this is that images and scripts with relative paths will not load correctly.
  • Michael
    Michael about 2 years
    Thanks for the code. There's enough here to guide down the path I want to go.