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 documentationdescribing 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:

{

"Authentication": {

"Facebook": {

"AppId": "YOUR-FACEBOOK-APP-ID",

"AppSecret": "YOUR-FACEBOOK-APP-SECRET"

}

},

"ConnectionStrings": {

"DefaultConnection": "Server=YOUR.SQL.SERVER-IP-ADDRESS;Database=webusers;User Id=aspnet;Password=YOUR-PASSWORD"

}

}
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.

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:

using Google.Cloud.Diagnostics.AspNetCore;

// ...

public class Startup

{

// ...

public void ConfigureServices(IServiceCollection services)

{

// Add Google Exception Logging to the services.

services.AddGoogleExceptionLogging(options =>

{

options.ProjectId = ProjectId;

options.ServiceName = "SocialAuthMVC";

options.Version = "0.01";

});

// ...

}



public void Configure(IApplicationBuilder app, IHostingEnvironment env)

{

if (env.IsDevelopment())

{

app.UseDeveloperExceptionPage();

app.UseDatabaseErrorPage();

}

else

{

// Show me the uncaught exceptions in Google's Stackdriver

// Error Reporting dashboard!

app.UseGoogleExceptionLogging();

app.UseExceptionHandler("/Home/Error");

app.UseHsts();

}

// ...

}

}

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:

  1. 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.
  2. 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.

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

I added one statement to Startup.cs to tell ASP.NET to use my IDataProtectionProvider:

             services.AddSingleton<IDataProtectionProvider,

KmsDataProtectionProvider>();

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:

My 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:

Aha! The redirect_uri was an http URI, but it should have been an httpsURI. Why did the ASP.NET auth middleware redirect to a http URI?

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.

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 Configure() method:

   // So middleware knows requests arrived via https.

app.UseForwardedHeaders(new ForwardedHeadersOptions()

{

ForwardedHeaders = ForwardedHeaders.XForwardedProto

});

I republished and redeployed my app to App Engine, clicked the Facebook button, and success at last!

Conclusions

The lessons learned in this exercise can be applied to make it easier to deploy most ASP.NET core application to Google AppEngine.

Lessons Learned

  1. Use app.UseForwardedHeaders() so middleware knows when the request arrived via HTTPS.
  2. Use Stackdriver Error Reporting to report uncaught exceptions.
  3. Use KmsDataProtectionProvider and Google Cloud Key Management Service so that an anti-request forgery token issued by one web server will not be rejected by other web servers.
  4. Use appsettings.json and Google Cloud Key Management Service to make it easy to maintain, deploy, and protect secrets.
  5. 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.

Rate this post