Posts About

Fixing Google OAuth issues in ASP.NET Core

Recently at CarbonCure we integrated Google OAuth into our backend. ASP.NET Core makes the process pretty easy, but we ran into a few gotchas I would like to share below.

"redirect_uri_mismatch"

If you get this error and Google auth mentions that <your-url>/signin-google is not in the list of authorized redirect URIs, then you simply need to add <your-url>/signin-google to that list in your OAuth credentials. Why this special URI/URL? It's an internal endpoint involved in authentication, and there's more info about that below.

"The oauth state was missing or invalid."

This error is a bit obscure and has a few potential causes. You can find this error in your logs.

Setting the callback path

This wasn't actually the cause of our issues, but I saw it discussed enough online to warrant talking about it.

One reason behind this error is that the ASP.NET Core authentication internals couldn't find a special involved in the inner workings of authentication. This is what the documentation is talking about when it mentions the option of setting a "default callback URI" (found here.) This certainly is not something you actually want to change!

This is the path assigned to a special endpoint internal to ASP.Net Core authentication. It's not something you are responsible for implementing, and it's not the same as the post-authentication callback you can specify in the challenge. My recommendation is to leave it be and not set it at all.

This helpful GitHub issue talks about this solution as well.

Requests being routed to different instances

The actual cause of our error was more subtle than the error above. We have two instances of our backend running behind a load balancer, which means requests will be "evenly" distributed between the two instances. During the OAuth process, the browser and your app need to talk back and forth with each other a few times. There is some state and caching involved in this conversation, so each request from the browser needs to go to the same running instance every time. This is incompatible with the behaviour of a load balancer, which will try its hardest to evenly distribute requests and ruin the above conversation. Since the requests are effectively split, and the instances don't share a cache, there's no way the OAuth process can complete, so the sign-in fails.

The easiest way to fix this is to enable sticky sessions on your load balancer. That way, the load balancer will send requests from a browser to the same instance every time. This will allow the auth process to complete. Note that this sort of defeats the purpose of having more than one application instance, since the instances are not truly sharing a workload. The robust, future-proof solution is to integrate some cache infrastructure like Redis between the instances. We don't quite need that yet, so we stuck with the the sticky sessions solution.

Internal redirect URI is http:// rather than https://

There's another small hiccup you may experience when using a load balancer. Typically, SSL is handled at the load balancer level and doesn't make its way to your ASP.NET Core instances since the load balancer will forward HTTP connections. This is for the best, since you won't have to deal with certificate management in your application and containers and fewer moving parts is always a good thing.

So, the load balancer is going to send HTTP requests, and since HTTPS is turned off, all incoming requests are going to have the HTTP protocol. This means all the handy URL request functions will also return HTTP, including that special signin-google endpoint I mentioned above. That's why Google auth will say http://<your-url>/signin-google isn't in the list of authorized URIs, even if you have the HTTPS address in that list.

Luckily the fix to this is easy. We can spoof usage of HTTPS by changing the scheme in some middleware, as demonstrated below.

public async Task InvokeAsync(HttpContext context)
{
    context.Request.Scheme = "https";
    await _next(context);
}

The above doesn't actually do any SSL stuff, but will force URL construction methods which use the incoming request to use our replaced scheme instead. This will effectively tell the client to use HTTPS endpoints everywhere without our app actually requiring HTTPS connections on the application level.