Adding Social Login to your ASP.NET core 2.1 Google Cloud Platform Application
Note, this is a refresh of a post I published in June of 2017. Since then, ASP.NET core has advanced and Google Cloud has advanced. It is now significantly easier to add social login to an ASP.NET core App Engine application. The code sample has also been updated to ASP.NET core 2.1. An additional bonus is that this new solution works for Google Kubernetes Engine too.
No More Passwords Please
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 the Facebook button 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, I needed to make a few tweaks 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 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. It would have been very convenient to store my Facebook AppSecret in
appsettings.json, but also very insecure.
So, I borrowed a solution from one of my earlier posts, put my secrets in
appsecrets.json, and encrypted them with Google Cloud Key Management Service:
PS > .\Encrypt-AppSecrets.ps1
I committed only the encrypted file,
appsecrets.json.encrypted, into my git repo and deployed it to App Engine. The App Engine environment contained the credentials necessary to decrypt the secrets. See my earlier post for details about how this solution keeps the secrets safe.
I republished and redeployed the app to App Engine, clicked the Facebook button,
And got a blank page with a 400 error:
Show me the error message.
I was looking at 400 with no error message. 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. But, I knew how to use Stackdriver Error Reporting to collect uncaught exceptions and report them to me in Google Cloud Dashboard. So, I added a dependency on Google.Cloud.Diagnostics.AspNetCore and added a few lines of code to my StartUp.cs:
I redeployed my application to App Engine, and clicked the Facebook button again. I saw the same 400 blank page, but now Stackdriver Error Reporting -showed me the full error message in Google Cloud Console:
Microsoft.AspNetCore.DataProtection tried to decrypt something, but failed. 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’s authors 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 creates its own unique keys. What’s more, it’s impossible to predict (by design) which web server a request will be routed to.
This becomes a problem in the situation diagrammed above:
- A request is routed to Web Server A, and Web Server A responds with an anti-request forgery token encrypted with Webserver A’s blue key.
- A POST request with the anti-request forgery token is routed to Web Server B, and Web Server B tries to decrypt the token with its pink key, but fails because its key is different.
Google has released libraries to solve this problem. Please see my new post discussing the problem and the new libraries that solve it.
I updated my code to use Google’s new libraries. Then, I republished and redeployed my app to App Engine, clicked the Facebook button again, and saw:
Hmmm. I double checked the whitelist of redirect URIs I registered with Facebook, and the URI I’m using is definitely in there:
appspot.com/signing-facebook URIs were known to Facebook. That was not the problem.
I inspected the
/oauth?client_id=… request coming from the browser more carefully:
redirect_uri was an
http URI, but it should have been an
https URI. Why did the ASP.NET auth middleware redirect to a
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:
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.
Thanks again to the developers building AspNetCore who seem to have the magic power to foresee all my problems.
app.UseForwardedHeaders() instructs ASP.NET middleware to rewrite requests based on
X-Forward-* headers. A request that arrived with an
X-Forwarded-Proto: https header gets rewritten to be identical to a request that arrived via https. I added one call to
UseForwardedHeaders() to my
I republished and redeployed my app to App Engine, clicked the Facebook button, and success at last!
The lessons learned in this exercise can be applied to make it easier to deploy most ASP.NET core application to Google AppEngine.
app.UseForwardedHeaders()so middleware knows when the request arrived via HTTPS.
- Use Stackdriver Error Reporting to report uncaught exceptions. See this earlier post for details.
Google.Cloud.AspNetCore.DataProtection.Kmsas demonstrated in my new post so that an anti-request forgery token issued by one web server will not be rejected by other web servers.
appsettings.jsonand Google Cloud Key Management Service to make it easy to maintain, deploy, and protect secrets. See this earlier post for details.
- Use SocialAuth as a template for new projects, to apply lessons 1–4 at once.
In fact, all these lessons are useful when deploying an application to other Google Cloud Platform services, like Kubernetes Engine and Compute Engine.
The full code with instructions to build and run are available on github.com.