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";
// ...
}
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();
}