Adding Social Login to your .NET App Engine Application

Users are tired of creating new logins and passwords for every website they visit. In 2013, a survey found that 86% of people said they are bothered by the need to create new accounts on websites. That number has probably grown even larger today. Personally, I count over 100 logins and passwords in my password manager.

A better solution, preferred by 77% of users according to the survey, is to allow users to login using their favorite social account, be it Facebook, Twitter, or Google. For users, social login removes a barrier to using your website. Saavn saw 65% higher engagement among listeners who use Facebook Login. Skyscanner saw impressive improvements in interaction as well.

The good news is that Microsoft has provided .NET programmers with libraries to ease the task of integrating social login into their .NET MVC core web applications. And they have provided excellent documentation describing how to add social login for all the popular providers. I followed the instructions in those docs and quickly added social login to my .NET core web app. I pressed F5 to run the application on my development machine, clicked Log in with Facebook and it worked! Yay!

Then I tried deploying it to Google App Engine and saw an error message:

Unhandled Exception: System.ArgumentException: The ‘ClientId’ option must be provided.
at Microsoft.AspNetCore.Authentication.Facebook.FacebookMiddleware..ctor(RequestDelegate next, IDataProtectionProvides`1 sharedOptions, IOptions`1 options)

Turns out, there are a few tweaks I needed to make to my MVC Web application to make social login work when running on App Engine. In fact, these tweaks are useful for running any .NET core MVC application on App Engine. I describe the issues and their solutions below.

App Engine doesn’t know my Facebook AppSecret.

To allow users to login with Facebook, I created a Facebook app as described in Microsoft’s docs. Facebook gave me an AppSecret, which is a secret string I pass to Facebook when I want to authenticate a user. When I ran the program on my development machine, I stored the secret using dotnet’s Secret Manager tool like this:

dotnet user-secrets set Authentication:Facebook:AppSecret <app-secret>

Of course, the dotnet command stores the secret locally on my development machine, and my application has no access to my development machine when it’s running on App Engine. When I ran the app on App Engine, the exception above complained because I passed a null Facebook ClientId to app.UseFacebookAuthentication(). I needed a secure way to distribute the AppSecret to my App Engine instances. I could save it in my appsettings.json, but then my AppSecret would get checked into git and end up on github.com for all to see. My AppSecret would immediately become not secret!

Metadata to the rescue! Google App Engine and Compute Engine allow you to store key value pairs in metadata, which is secure and can be read by app engine instances. I set the metadata with commands like this:

gcloud compute project-info add-metadata — metadata=$key=$value

Actually, I wrote a script to copy all the relevant user secrets to metadata. Then, I wrote a ConfigurationProvider that reads the metadata when running on App Engine.

The new code solved this problem. I successfully deployed to App Engine, but then when I visited my app’s home page, I saw an error:

App Engine and HTTPS

Social login providers require HTTPS connections to prevent a variety of attacks. To force browsers to connect to my app via HTTPS, I added the following three lines of code to Startup.cs:

var rewriteOptions = new RewriteOptions();
rewriteOptions.AddRedirectToHttps(302, 44393);
app.UseRewriter(rewriteOptions);

That worked fine when running locally, but failed when I deployed to App Engine because the App Engine load balancer listens on HTTPS’s default port 443. So, I changed the code to redirect HTTP requests to port number 443, recompiled and deployed my app and saw:

What is going on?

App Engine makes HTTPS look like HTTP.

When I typed https://my-app.appspot.com/ into my browser, my app saw a request with the following properties:

Scheme: http
X-Forwarded-Proto: https

As described in Google’s App Engine docs:

The Google Cloud Load Balancer terminates all https connections, and then forwards traffic to App Engine instances over http. For example, if a user requests access to your site via https://[MY-PROJECT-ID].appspot.com, the X- Forwarded-Proto header value is https.

When my app received an HTTPS request, the Rewriter did not see it as HTTPS. Instead, it saw Scheme: http and decided it still needed to redirect the request to HTTPS. So, it redirected the request to exactly the same url it was requesting, resulting in an infinite loop of redirects. I needed to make the Rewriter understand that the incoming requests were actually HTTPS.

Thanks to the good developers building AspNetCore who provided a way to rewrite incoming requests in a custom way. To rewrite the incoming request, I implemented a Microsoft.AspNetCore.Rewrite.IRule that inspects X-Forwarded-Proto. When the X-Forwarded-Proto is set to https, my IRule resets the scheme to https.

string proto = request.Headers[“X-Forwarded-Proto”]
.FirstOrDefault();
if (proto == “https”) {
request.IsHttps = true;
request.Scheme = “https”;
return true;

With this new IRule in place, I redeployed my app to App Engine and HTTPS started working. Yay. I clicked Log in with Facebook, and saw this error page:

Show me the error message.

As you can see above, the error page hid all the important details. I needed to see the details in order to debug this issue. I didn’t want to deploy a Development Mode application to App Engine because that would risk exposing my secrets. So, I added a dependency on Google.Cloud.Diagnostics.AspNetCore and added one statement to my StartUp.cs:

services.AddGoogleExceptionLogging(projectId,
Configuration[“GoogleErrorReporting:ServiceName”],
Configuration[“GoogleErrorReporting:Version”]);

Then, I could see the full error message in the Google Cloud Console:

Microsoft.AspNetCore.Dataprotection tried to decrypt something, but it can’t find the key. What’s going on?

AspNetCore’s default DataProtectionProvider does not work across multiple web server instances.

Social login middleware like Microsoft.AspNetCore.Authentication.Facebook needs encryption. Again, AspNetCore did a great job of foreseeing this need and provided a standard interface, IDataProtectionProvider to provide encryption. The problem, though, is that the default IDataProtectionProvider stores encryption keys locally on the web server. However, on App Engine, there’s always more than one web server, and each web server ends up with different keys. What’s more, it’s impossible to predict (by design) to which web server a request will be routed.

This becomes a problem when:

  1. A request is routed to Web Server 1, and Web Server 1 responds with a new cookie encrypted with A Key.
  2. A subsequent request with the encrypted cookie is routed to Web Server 2, and Web Server 2 tries to decrypt the cookie with A Key, but does not have an A Key.

Fortunately, Google created Cloud Key Management Service to solve exactly this kind of problem. So I implemented my own IDataProtectionProvider that uses Google KMS to manage keys.

I redeployed my app to App Engine, clicked Log in with Facebook one more time and I saw SUCCESS!

Conclusions

It took some work to get my app to work correctly on Google App Engine, but the result is an application that keeps its secrets secret and scales automatically as load increases. Pretty nice. And thanks to the AspNetCore’s well designed interfaces and features and Google’s well designed services, no hacks were required! Just a bit of work to get things to work together.

The Cloud Tools for Visual Studio team at Google is working hard to build all these solutions into a new library and a new project template. When it is released, you will be able to choose a new App Engine Flex MVC Project in Visual Studio and enjoy all these integrations with no additional work.

Until then, you can build and run my project yourself! Feel free to browse the full code in our github repository and file issues in that repo.