Search code examples
c#asp.net-coreoauthoracle-cloud-infrastructure

Issues authenticating ASP.NET Core via Oracle Oauth when site is behid WAF/Gateway


I have a front-end (JavaScript) application which uses a C# .NET back-end. Authentication is implemented via OAuth (Oracle IDCS). It was working fine, but now the back-end app is behind an API gateway/WAF (which is accessible via the Internet). Let's say the public host is public-host.com. The gateway translates the public address to a local host that my back-end application is receiving requests on (so my back-end "thinks" it's hosted under a different URL than it actually is). Let's say the private host is private-host.io. Now the back-end is using wrong redirect_uri OAuth parameter when calling Oracle (https://idcs-xxx.identity.oraclecloud.com/oauth2/v1/authorize?client_id=...&scope=...&...&redirect_uri=http://private-host.io/authorization_code/callback&... - example without url encoding). Publicly it's https but locally/internally it's http.

I attempted to fix this by:

        services
            .AddAuthentication(options =>
            {
                /*...*/
            })
            .AddOAuth("Oracle", options =>
            {
                options.Events = new OAuthEvents
                {
                    OnRedirectToAuthorizationEndpoint = context =>
                    {
                        // Replace the `redirect_uri` query string parameter found in `context.RedirectUri` with the public one
                        // so replace "http://internal-host.io/" with "https://public-host.com/" when redirecting to Oracles OAuth endpoint
                        // context.RedirectUri holds the full redirect URL like: https://idcs-xxx.identity.oraclecloud.com/oauth2/v1/authorize?client_id=...&scope=...&...&redirect_uri=http://internal-host.io/authorization_code/callback
                        // this is more of a pseudo-code because we have to deal with encoding the URL/query string
                        context.RedirectUri = context.RedirectUri.Replace("http://internal-host.io/", "https://public-host.com/");
                        context.Response.Redirect(context.RedirectUri);
                        return Task.CompletedTask;
                    }
                };
            });

Now the correct redirect_uri parameter is used when sending the OAuth request, the authentication in Oracle is a success (I think) and Oracle redirects back to the correct address (https://public-host.com/authorization-code/callback?code=XXX but now the C# code throws two errors:

An error was encountered while handling the remote login. Correlation failed.
Microsoft.AspNetCore.Authentication.AuthenticationFailureException:
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1+<HandleRequestAsync>d__12.MoveNext (Microsoft.AspNetCore.Authentication, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)

and

Microsoft.AspNetCore.Authentication.AuthenticationFailureException: An error was encountered while handling the remote login.
---> Microsoft.AspNetCore.Authentication.AuthenticationFailureException: OAuth token endpoint failure: invalid_redirect_uri;Description=Client XXXX requested an invalid redirect URL: http://internal-host.io:443/authorization-code/callback.

(please note the http://internal-host.io)

So my suspicion is that despite me changing the redirect_uri in the OnRedirectToAuthorizationEndpoint, the OAuth handling code somehow expects the local-host.io instead of the public-host.com and fails because it's something different...


Some more configuration:

services.AddAuthentication(options => {...}) 
.AddOAuth("Oracle", options =>
{
    Uri apiAddress = configuration.OracleApiAddress;
    options.AuthorizationEndpoint = $"{apiAddress}/oauth2/v1/authorize";
    options.Scope.Add("openid");
    options.Scope.Add("urn:opc:idm:__myscopes__");
    options.CallbackPath = new PathString("/authorization-code/callback");
    options.ClientId = configuration.OracleApplicationId;
    options.ClientSecret = configuration.OracleApplicationSecret;
    options.TokenEndpoint = $"{apiAddress}/oauth2/v1/token";
    options.UserInformationEndpoint = $"{apiAddress}/oauth2/v1/userinfo";
    // ...
}

Solution

  • Thanks to comment by Jason Pan I ended up using a .NET built-in method of replacing scheme/host/port based on the request headers: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-8.0#forwarded-headers-middleware-order

        public static void Main(string[] args)
        {
            WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
            /* ... */
            builder.Services.Configure<ForwardedHeadersOptions>(options =>
            {
                // I was only interested in the protocol (http/https) and the host.
                // Had to figure out the correct headers based on my infrastructure (Azure)
                options.ForwardedHostHeaderName = "x-original-host";
                options.OriginalProtoHeaderName = "x-forwarded-proto";
                options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
            });
            /* ... */
            WebApplication app = builder.Build();
            app.UseForwardedHeaders();
            /* ... */
    
            app.Run();
        }
    
    

    Old answer:

    The solution was to modify the request host/scheme/port in a custom middleware. In my case the external/original host was known thanks to headers included by the API gateway.

    public class CustomMiddleware(RequestDelegate next)
    {
        public async Task InvokeAsync(HttpContext context)
        {
            // Header from the API gateway
            string originalHost = context.Request.Headers["x-original-host"].ToString();
            context.Request.Scheme = "https";
            context.Request.Host = new HostString(originalHost, 443);
            await next(context);
        }
    }
    

    Usage in Program.cs

        public static void Main(string[] args)
        {
            WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
            /* ... */
            WebApplication app = builder.Build();
            app.UseMiddleware<CustomMiddleware>();
            /* ... */
            app.UseAuthentication();
            app.UseAuthorization();
            /* ... */
            app.Run();
        }