Extend Microsoft.AspNetCore.Authentication.OAuth to Support Reverse Proxy

Recently, one customer said his Asp.net Core application used to work very well in OAuth authentication all the time, but after he placed a reverse proxy in front of the web application, the authentication fails because of invalid callback path.

Normally speaking, a callback path is needed when registering an application to the OAuth server. The application will send a callback path to the OAuth server during the authentication workflow as well, OAuth server will verify the client id, client secret and the call back url to make sure the application is a registered application.

The following is the snippet code to use Microsoft.AspNetCore.Authentication.OAuth module for OAuth authentication in Asp.net Core application.

 public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "oauth";
        })
        .AddCookie()
        .AddOAuth("oauth", options =>
        {
            options.ClientId = Configuration["ClientId"];
            options.ClientSecret = Configuration["ClientSecret"];
            options.CallbackPath = new PathString(Configuration["CallbackPage"]); 

            options.AuthorizationEndpoint = Configuration["AuthorizationEndpoint"];
            options.TokenEndpoint = Configuration["TokenEndpoint"];
            options.UserInformationEndpoint = Configuration["UserInformationEndpoint"];

            ...
        });
}

The options.CallbackPath in the above snippet code is PathString type which only accepts relative path begin with '/'. The Microsoft.AspNetCore.Authentication.OAuth module will call the BuildRedirectUri shown below in AuthenticationHandler.cs to build the callback url based on the relative callback path provided in options.CallbackPath.

 
protected string BuildRedirectUri(string targetPath)
=> Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath;

While in a reverse proxy scenario, let's suppose the web application's raw domain name is internal.com, the callback url registered into OAuth Server is different such as contoso.com, the domain name of the reverse proxy. Depending on different proxy, some proxy could pass raw domain (internal.com) in the http request's host name instead of contoso.com to the application. Furthermore, if SSL offloading is enabled, the http request schema will be http though end user could use https. That means, the callback registered in OAuth server is https://contoso.com while the application is sending https://internal.com for verification.

Luckily, most reverse proxy has provided several header to record original information such as X-Forwarded-Host and X-Forwarded-Proto or equivalent identifier. In order to support reverse proxy, we can extend Microsoft.AspNetCore.Authentication.OAuth to use X-Forwarded-Host and X-Forwarded-Proto to build the callback path. Lastly, make sure call AddMyOAuth instead of AddOAuth in ConfigureServices.

 
public static class MyOAuthExtensions
{
        public static AuthenticationBuilder AddMyOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions)
            => builder.AddOAuth<OAuthOptions, MyOAuthHandler>(authenticationScheme, configureOptions);
}


public class MyOAuthHandler : OAuthHandler where TOptions : OAuthOptions, new()
{
        public MyOAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
            : base(options, logger, encoder, clock)
        { }

        // Copy from https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs
        protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            if (string.IsNullOrEmpty(properties.RedirectUri))
            {
                properties.RedirectUri = CurrentUri;
            }

            // OAuth2 10.12 CSRF
            GenerateCorrelationId(properties);

            var authorizationEndpoint = BuildChallengeUrl(properties, BuildRedirectUri(Options.CallbackPath));
            var redirectContext = new RedirectContext(
                Context, Scheme, Options,
                properties, authorizationEndpoint);
            await Events.RedirectToAuthorizationEndpoint(redirectContext);
        }

        protected new string BuildRedirectUri(string targetPath)
        {
            var schema = Request.Headers["X-Forwarded-Proto"].Count > 0 ? Request.Headers["X-Forwarded-Proto"][0] : Request.Scheme;
            var host = = Request.Headers["X-Forwarded-Host"].Count > 0 ? Request.Headers["X-Forwarded-Host"][0] : Request.Host;
            return schema + "://" + host + OriginalPathBase + targetPath;         
        }
}