asp.netasp.net-coremigrationmicrosoft-yarpreturnurl

SystemWeb Adapters Authentication failing due to multiple redirects with "query string is too long"


I am facing a problem when using systemweb adapters and YARP to incrementally migrate from ASP.NET on .NET 4.8 to ASP.NET Core 8.0.

Specifically I am using authentication client and server and when I browse the home page before authentication on a mobile device, the request URL becomes too long due to recursive addition of ReturnUrls to it.

The URL looks like this:

http://localhost:8080/account/login?ReturnUrl=%2fsystemweb-adapters%2fauthenticate%3foriginal-url%3d%252Faccount%252Flogin%253FReturnUrl%253D%25252fsystemweb-adapters%25252fauthenticate%25253foriginal-url%25253d%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252fsystemweb-adapters%252525252fauthenticate%252525253foriginal-url%252525253d%25252525252Faccount%25252525252Flogin%25252525253FReturnUrl%25252525253D%2525252525252fsystemweb-adapters%2525252525252fauthenticate%2525252525253foriginal-url%2525252525253d%252525252525252F%252525252526original-url%25252525253D%2525252525252F%25252525252C%25252525252Faccount%25252525252Flogin%25252525253FReturnUrl%25252525253D%2525252525252f%25252526original-url%2525253D%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252fsystemweb-adapters%25252525252fauthenticate%25252525253foriginal-url%25252525253d%2525252525252F%2525252526original-url%252525253D%25252525252F%252525252C%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252f%2525252C%2525252Fmobile%2525252Flogin%2526original-url%253D%25252Faccount%25252Flogin%25253FReturnUrl%25253D%2525252fsystemweb-adapters%2525252fauthenticate%2525253foriginal-url%2525253d%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252fsystemweb-adapters%25252525252fauthenticate%25252525253foriginal-url%25252525253d%2525252525252F%2525252526original-url%252525253D%25252525252F%252525252C%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252f%252526original-url%25253D%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252fsystemweb-adapters%252525252fauthenticate%252525253foriginal-url%252525253d%25252525252F%25252526original-url%2525253D%252525252F%2525252C%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252f%25252C%25252Fmobile%25252Flogin%252C%252Fmobile%252Flogin&original-url=%2Faccount%2Flogin%3FReturnUrl%3D%252fsystemweb-adapters%252fauthenticate%253foriginal-url%253d%25252Faccount%25252Flogin%25253FReturnUrl%25253D%2525252fsystemweb-adapters%2525252fauthenticate%2525253foriginal-url%2525253d%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252fsystemweb-adapters%25252525252fauthenticate%25252525253foriginal-url%25252525253d%2525252525252F%2525252526original-url%252525253D%25252525252F%252525252C%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252f%252526original-url%25253D%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252fsystemweb-adapters%252525252fauthenticate%252525253foriginal-url%252525253d%25252525252F%25252526original-url%2525253D%252525252F%2525252C%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252f%25252C%25252Fmobile%25252Flogin%26original-url%3D%252Faccount%252Flogin%253FReturnUrl%253D%25252fsystemweb-adapters%25252fauthenticate%25253foriginal-url%25253d%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252fsystemweb-adapters%252525252fauthenticate%252525253foriginal-url%252525253d%25252525252F%25252526original-url%2525253D%252525252F%2525252C%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252f%2526original-url%253D%25252Faccount%25252Flogin%25253FReturnUrl%25253D%2525252fsystemweb-adapters%2525252fauthenticate%2525253foriginal-url%2525253d%252525252F%252526original-url%25253D%2525252F%25252C%25252Faccount%25252Flogin%25253FReturnUrl%25253D%2525252f%252C%252Fmobile%252Flogin%2C%2Fmobile%2Flogin

This is the code on the ASP.NET Core side:

using WebCore;
using Yarp.ReverseProxy.Forwarder;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // <snip>
    })
    .AddRemoteAppClient( options =>
    {
        options.RemoteAppUrl = new(builder.Configuration["ProxyTo"]);
        options.ApiKey = builder.Configuration["ProxyApiKey"];
    })
    .AddAuthenticationClient(isDefaultScheme: true)
    .AddSessionClient();

builder.Services.AddHttpForwarder();
builder.Services.AddDetection();

builder.Services.AddWebOptimizer(//<snip>);

// <snip>

var app = builder.Build();

var forwarderRequestConfig = builder.Configuration.GetSection("ForwarderRequestConfig")
    .Get<ForwarderRequestConfig>();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

app.UseCors();

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthorization();
app.UseSystemWebAdapters();

app.MapForwarder("/{**catch-all}", app.Configuration["ProxyTo"], forwarderRequestConfig, new ForwardOriginalUrlTransformer())
    .Add(static builder => ((RouteEndpointBuilder)builder).Order = int.MaxValue);

app.MapDefaultControllerRoute()
    .RequireSystemWebAdapterSession();

app.Run();

On the ASP.NET side, I have an AccountsController for login that looks like this:

[Mobility]
public ActionResult Login(string returnUrl = "", string mode = "")
{
    SecurityService.Logout();

    if (returnUrl.Contains("/account/login"))
        return RedirectToAction("login", "account", new { mode = "login" });

    var model = new
                {
                    Mode = mode,
                    RedirectUrl = returnUrl,
                    ValidationLiterals = AccountFacade.ValidationLiterals()
                };

    return View(model.ToJson());
}

And the Mobility attribute is coded as:

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    // Determine if we've processed their preference already via the UI site-switching links
    var useMobileValue = HttpContext.Current.Session["UseMobile"];

    bool useMobile;
    // <snip>

    if (useMobile)
    {
        var url = "/mobile";

        if (!Global.Authenticated)
            url += "/login";

        filterContext.Result = new RedirectResult(url);
    }
}

Thanks for your time.

I was expecting this work by default but it doesn't. I have tried adding a middleware to strip off extra ReturnUrls on the ASP.NET Core side, but it is getting more and more complicated as I code it and it is not helping.

I am wondering if the multiple redirects is causing problems with the auth flow. Is there a work around for this? Will it help me if I migrate the home and accounts controller over to ASP.NET Core, but in this case, how do I set the login path?


Solution

  • I fixed this using the following code

    public override async ValueTask<bool> TransformResponseAsync(HttpContext context, HttpResponseMessage proxyResponse)
    {
        // Allow redirect response to pass through
        if (proxyResponse?.StatusCode == HttpStatusCode.Found &&
            proxyResponse?.Headers?.Location?.ToString()?.EndsWith("/mobile/login", StringComparison.OrdinalIgnoreCase) == true)
        {
            context.Response.StatusCode = (int)proxyResponse.StatusCode;
            context.Response.Headers.Location = proxyResponse.Headers.Location?.ToString();
            return false; // Prevent YARP from processing the redirect
        }
    
        // Allow default processing for other responses
        return await HttpTransformer.Default.TransformResponseAsync(context, proxyResponse);
    }