Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC 5 Owin Facebook Auth results in Null Reference Exception

I'm trying to setup integrated OWIN Facebook authentication in a new MVC 5 project in Visual Studio 2013. I have configured apps and keys as per this tutorial:

http://www.asp.net/mvc/tutorials/mvc-5/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on

However, I'm getting a NullReferenceException thrown from this call in the AccountController:

    [AllowAnonymous]
    public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
    {
        var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();

I already checked the response in Fiddler and am getting what appears to be a success response from Facebook, but still get this error. The response looks like this:

{"id":"xxx","name":"xxx","first_name":"xxx","last_name":"xxx","link":
"https:\/\/www.facebook.com\/profile.php?id=xxx","location":{"id":"xxx","name":"xxx"},
"gender":"xxx","timezone":1,"locale":"en_GB","verified":true,"updated_time":"2013-10-23T10:42:23+0000"}

I get this when debugging in http as well as https. I'm guessing this is a framework bug but have so far drawn a blank diagnosing this through reflector.

like image 941
Gracie Avatar asked Oct 24 '13 11:10

Gracie


4 Answers

This probably is a bug in identity OWIN extension code. I can't repro the issue as my facebook payload always returns a username field in json, which is missing from your fb response. I am not quite sure why it's not there.

The code in identity owin extension method doesn't have a null check for the identity's name claim which is same as the username field. We have filed a bug for it internally.

In order to workaround this issue, could you try replacing your ExternalLoginCallback method with following code:

   [AllowAnonymous]
    public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
    {
        var result = await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie);
        if (result == null || result.Identity == null)
        {
            return RedirectToAction("Login");
        }

        var idClaim = result.Identity.FindFirst(ClaimTypes.NameIdentifier);
        if (idClaim == null)
        {
            return RedirectToAction("Login");
        }

        var login = new UserLoginInfo(idClaim.Issuer, idClaim.Value);
        var name = result.Identity.Name == null ? "" : result.Identity.Name.Replace(" ", "");

        // Sign in the user with this external login provider if the user already has a login
        var user = await UserManager.FindAsync(login);
        if (user != null)
        {
            await SignInAsync(user, isPersistent: false);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            // If the user does not have an account, then prompt the user to create an account
            ViewBag.ReturnUrl = returnUrl;
            ViewBag.LoginProvider = login.LoginProvider;
            return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { UserName = name });
        }
    }

The code will set default user name as empty when there is no username back from facebook/google.

like image 115
Hongye Sun Avatar answered Nov 17 '22 05:11

Hongye Sun


Hongye Sun did all the heavy lifting in his answer above.

Here's some code that can be added to your controller class and be called in place of the troublesome AuthenticationManager.GetExternalLoginInfoAsync().

private async Task<ExternalLoginInfo> AuthenticationManager_GetExternalLoginInfoAsync_Workaround()
{
    ExternalLoginInfo loginInfo = null;

    var result = await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie);

    if (result != null && result.Identity != null)
    {
        var idClaim = result.Identity.FindFirst(ClaimTypes.NameIdentifier);
        if (idClaim != null)
        {
            loginInfo = new ExternalLoginInfo()
            {
                DefaultUserName = result.Identity.Name == null ? "" : result.Identity.Name.Replace(" ", ""),
                Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value)
            };
        }
    }
    return loginInfo;
}
like image 32
Bryan Knox Avatar answered Nov 17 '22 07:11

Bryan Knox


I had the same problem. I solve my problem just added app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create); to the Startup.Auth.cs. I didn't have that in my Startup.Auth.cs so

var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);

always thrown me an Object reference not set to an instance of an object error. I figure that out by analyzing VS 2013 default template for MVC 5. So if you need more info on code structure or example take a look on VS 2013 MVC5 template.

like image 4
Daniil T. Avatar answered Nov 17 '22 05:11

Daniil T.


I came across this post a few days ago but unfortunately none of the above solutions worked for me. so here is how I managed to fix it and get the email from Facebook.

  • Update following NuGet Pacakges
    • Microsoft.Owin to version 3.1.0-rc1
    • Microsoft.Owin.Security to version 3.1.0-rc1
    • Microsoft.Owin.Security.Cookies to version 3.1.0-rc1
    • Microsoft.Owin.Security.OAuth to version 3.1.0-rc1
    • Microsoft.Owin.Security.Facebook to version 3.1.0-rc1

Then add the following code to the Identity Startup class

var facebookOptions = new FacebookAuthenticationOptions()
        {
            AppId = "your app id",
            AppSecret = "your app secret",
            BackchannelHttpHandler = new FacebookBackChannelHandler(),
            UserInformationEndpoint = "https://graph.facebook.com/v2.8/me?fields=id,name,email,first_name,last_name",
            Scope = { "email" }
        };

        app.UseFacebookAuthentication(facebookOptions);

This is the definition class for FacebookBackChannelHandler():

using System;
using System.Net.Http;

public class FacebookBackChannelHandler : HttpClientHandler
{
    protected override async System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        System.Threading.CancellationToken cancellationToken)
    {
        // Replace the RequestUri so it's not malformed
        if (!request.RequestUri.AbsolutePath.Contains("/oauth"))
        {
            request.RequestUri = new Uri(request.RequestUri.AbsoluteUri.Replace("?access_token", "&access_token"));
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
like image 3
user3012760 Avatar answered Nov 17 '22 05:11

user3012760