Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

401 Response from ASP.NET Identity when linking to External Accounts


:P
On this page:

This falls under ‘stupid developer errors’, but I thought I’d post it anyway, since this might help out somebody who accidentally misconfigured external Identity providers as I did.

In my last post I talked about how to hook up a minimal ASP.NET Identity implementation to an existing application and showed how you can link and login through external accounts.

Just before I had completed the article I made a few changes to my actual implementation for the live site, removing the secure secret provider keys from the main configuration file and storing them in a separate CodePasteKeys.json file as part of my AppConfiguration settings class. The idea was to isolate the keys and not put those into the GitHub repository for all to see. The file is not part of the project and explicitly excluded in .gitignore. But… it also isn’t published by WebDeploy. You’re probably seeing where this is going, right?

I got distracted with some other work and forgot to move the configuration file to the server. Next day I came back, made a few unrelated changes, published and called it a day. Later that day I logged in and quickly realized that my Google login wasn’t working. WTF? It works just fine locally.

Of course I had already long forgotten about externalizing the login credentials, but my startup code that sets up the external configuration providers depends on those values being there:

// these values are stored in CodePasteKeys.json
// and are NOT included in repro - autocreated on first load
if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
{
    app.UseGoogleAuthentication(
        clientId: App.Secrets.GoogleClientId,
        clientSecret: App.Secrets.GoogleClientSecret);
}

if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey))
{
    app.UseTwitterAuthentication(
        consumerKey: App.Secrets.TwitterConsumerKey,
        consumerSecret: App.Secrets.TwitterConsumerSecret);
}

if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId))
{
    app.UseGitHubAuthentication(
        clientId: App.Secrets.GitHubClientId,
        clientSecret: App.Secrets.GitHubClientSecret);
}

The App.Secrets object exists but because I never entered the values in the file on the server,  all the values are null by default. In short, none of the providers were configured.  Again: DUH!

Unconfigured Providers result in Empty 401 Responses

Here’s the relevant part though: You don’t see any sort of error or failure until you try to login with one of the providers that now aren’t configured. It makes perfect sense that this would wouldn’t work, but unfortunately the failure is not easy to detect.

When you try to call the ExternalLogin controller action:

[AllowAnonymous]        
[HttpPost]        
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider)
{
    string returnUrl = Url.Action("New", "Snippet", null);           

    return new ChallengeResult(provider,
        Url.Action("ExternalLoginCallback", "Account", 
        new {ReturnUrl = returnUrl}));
}

The result merely returns a 401 Unauthorized response. No error, no explicit failure, but just a 401with an empty response body.

What it should be returning is  302 Moved operation, which redirects to the external provider, makes you login or simply returns your existing login if you are already logged in with your external account.

The bottom line is: If you see a 401 response from your external login provider, make sure that you have your external login providers properly configured in your startup code!

Give me some Notification!

This feels like a bug to me. While 401 is the proper HTTP response to return to the browser, I think this should do more than return an empty response body. It would be nice if the response body gave at least some indication – even if vague -  what was happening. Something like “Provider not configured” seems appropriate. Alternately it would also be OK if ASP.NET would actually throw if the provider you are trying to access is not there. That way I can at least capture the error and be notified if an error occurred on the server. Any failure that occurs without some way to catch the issue is pretty insidious.

My workaround for now is to throw an exception when the application starts in the Startup.Configure() code to check for at least the Google provider key. If the key is null throw which will make the application fail hard on startup:

ExternalProviderError 

Pretty drastic – but also pretty hard to miss when you’ve messed this up, which is exactly the opposite – and appropriate - behavior of what the default implementation does.

Related Posts

Posted in ASP.NET  MVC  


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024