66

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?

Gimly
  • 5,341
  • 3
  • 38
  • 69
  • IIS reverse proxy – Callum Linington Feb 02 '17 at 10:58
  • But what if you're not hosting on IIS? I might go the road of hosting using Kestrel on a Docker image or something like that. – Gimly Feb 02 '17 at 11:01
  • You can use any server as reverse proxy. So spin up a express app with reverse proxy or any other popular web server with reverse proxy... – Callum Linington Feb 02 '17 at 11:03
  • 1
    I can't really see how that's going to be implemented. How do I then check the auth token from the reverse proxy? – Gimly Feb 02 '17 at 11:09
  • I don't really see any problem with your code to be honest, I would just abstract it out. and make sure that you explicitly copying any headers or query string values over to protect yourself from exploits. – Callum Linington Feb 02 '17 at 11:11
  • you can try the NetCoreStack Flying Proxy - https://github.com/NetCoreStack/Proxy – Gencebay Aug 11 '17 at 08:07
  • Useful tip: [creating-a-proxy-with-apicontroller](https://philsversion.com/2012/09/06/creating-a-proxy-with-apicontroller/) – J.Hpour Jun 26 '18 at 09:22
  • Microsoft solution Yarp.ReverseProxy - https://microsoft.github.io/reverse-proxy/articles/getting_started.html – surya May 20 '21 at 14:29

8 Answers8

31

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}");
}
twitchax
  • 639
  • 5
  • 13
  • 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. – James Lawruk Jan 11 '19 at 16:13
  • Check out the solution below. The middleware does not take the class route into account yet. Feel free to file an issue! :) – twitchax Jan 21 '19 at 10:44
20

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.

Gimly
  • 5,341
  • 3
  • 38
  • 69
  • 1
    could you share your implements of your middleware? If it possible. Is it strongly based on .Net Core? Thanks. – Dmitriy Nov 28 '17 at 01:14
  • @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 https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs file to get an idea on how to start the middleware. – Gimly Nov 28 '17 at 14:14
7

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)

Anduin
  • 1,982
  • 2
  • 13
  • 31
5

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.

baumgarb
  • 1,416
  • 1
  • 17
  • 23
  • 1
    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();` – Eddie Jan 31 '20 at 20:23
4

I had luck using twitchax's AspNetCore.Proxy NuGet package, but could not get it to work using the ProxyRoute method shown in twitchax's answer. (Could have easily been a mistake on my end.)

Instead I defined the mapping in Statup.cs Configure() method similar to the code below.

app.UseProxy("api/someexternalapp-proxy/{arg1}", async (args) =>
{
    string url = "https://someexternalapp.com/" + args["arg1"];
    return await Task.FromResult<string>(url);
});
James Lawruk
  • 26,651
  • 19
  • 117
  • 128
4

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!

2

Here is a basic implementation of Proxy library for ASP.NET Core:

This does not implement the authorization but could be useful to someone looking for a simple reverse proxy with ASP.NET Core. We only use this for development stages.

using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

namespace Sample.Proxy
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging(options =>
            {
                options.AddDebug();
                options.AddConsole(console =>
                {
                    console.IncludeScopes = true;
                });
            });

            services.AddProxy(options =>
            {
                options.MessageHandler = new HttpClientHandler
                {
                    AllowAutoRedirect = false,
                    UseCookies = true 
                };

                options.PrepareRequest = (originalRequest, message) =>
                {
                    var host = GetHeaderValue(originalRequest, "X-Forwarded-Host") ?? originalRequest.Host.Host;
                    var port = GetHeaderValue(originalRequest, "X-Forwarded-Port") ?? originalRequest.Host.Port.Value.ToString(CultureInfo.InvariantCulture);
                    var prefix = GetHeaderValue(originalRequest, "X-Forwarded-Prefix") ?? originalRequest.PathBase;

                    message.Headers.Add("X-Forwarded-Host", host);
                    if (!string.IsNullOrWhiteSpace(port)) message.Headers.Add("X-Forwarded-Port", port);
                    if (!string.IsNullOrWhiteSpace(prefix)) message.Headers.Add("X-Forwarded-Prefix", prefix);

                    return Task.FromResult(0);
                };
            });
        }

        private static string GetHeaderValue(HttpRequest request, string headerName)
        {
            return request.Headers.TryGetValue(headerName, out StringValues list) ? list.FirstOrDefault() : null;
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseWebSockets()
                .Map("/api", api => api.RunProxy(new Uri("http://localhost:8833")))
                .Map("/image", api => api.RunProxy(new Uri("http://localhost:8844")))
                .Map("/admin", api => api.RunProxy(new Uri("http://localhost:8822")))
                .RunProxy(new Uri("http://localhost:8811"));
        }

        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}
Kerem Demirer
  • 996
  • 1
  • 10
  • 24
  • 2
    Can you update the nuget package this code does not work with published 0.2.0 – Kugel Jan 18 '18 at 01:54
  • 2
    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? – Allan Jan 20 '18 at 00:31
  • 1
    I used v0.2 nuget from preview feeds: https://dotnet.myget.org/feed/aspnetcore-release/package/nuget/Microsoft.AspNetCore.Proxy – Kerem Demirer Jan 26 '18 at 07:40
  • I have the same problem with @Allan. Is any solution for it? – Bangyou Mar 07 '18 at 02:49
  • 2
    It seems the SDK not support asp net core 2.1 when I compile the source codes. – Bangyou Mar 07 '18 at 03:29
  • Same thing.cannot resolve services.AddProxy() function with Microsoft.AspNetCore.Proxy v0.2.0 ... Since March 2018, can anyobody give us the solution?? – amin89 Apr 17 '18 at 07:50
  • @amin89 The method name was changed to [RunProxy](https://github.com/aspnet/Proxy/blob/f127246246be358b3aa8c1b7c197ff496854dd08/src/Microsoft.AspNetCore.Proxy/ProxyExtensions.cs#L15-L20). It's not clear if the feature is experimental or not https://github.com/aspnet/Home/issues/2931 – JPelletier May 29 '18 at 13:26
  • @Allan I found it if u still need the solution. – amin89 May 29 '18 at 15:33
2

Twitchax's answer seems to be the best solution at the moment. In researching this, I found that Microsoft is developing a more robust solution that fits the exact problem the OP was trying to solve.

Repo: https://github.com/microsoft/reverse-proxy

Article for Preview 1 (they actually just released prev 2): https://devblogs.microsoft.com/dotnet/introducing-yarp-preview-1/

From the Article...

YARP is a project to create a reverse proxy server. It started when we noticed a pattern of questions from internal teams at Microsoft who were either building a reverse proxy for their service or had been asking about APIs and technology for building one, so we decided to get them all together to work on a common solution, which has become YARP.

YARP is a reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET. The key differentiator for YARP is that it is being designed to be easily customized and tweaked to match the specific needs of each deployment scenario. YARP plugs into the ASP.NET pipeline for handling incoming requests, and then has its own sub-pipeline for performing the steps to proxy the requests to backend servers. Customers can add additional modules, or replace stock modules as needed.
...
YARP works with either .NET Core 3.1 or .NET 5 preview 4 (or later). Download the preview 4 (or greater) of .NET 5 SDK from https://dotnet.microsoft.com/download/dotnet/5.0

More specifically, one of their sample apps implements authentication (as for the OP's original intent) https://github.com/microsoft/reverse-proxy/blob/master/samples/ReverseProxy.Auth.Sample/Startup.cs

spencer741
  • 350
  • 3
  • 15