为什么使用Identity和OAuth 2的MVC 5会在新的FB API 2.4中返回空电子邮件?

46

一切都很完美,直到fb将其api升级到2.4(我在之前的项目中使用的是2.3)。

今天,当我在fb开发者平台上添加新应用程序时,它会自动分配api 2.4。

问题:现在我从fb获取空电子邮件 (loginInfo.email = null)。

当然,我检查了用户电子邮件在fb个人资料中是否为公共状态,

我检查了loginInfo对象,但没有找到其他电子邮件地址。

我在谷歌上搜索了这个问题,但没有找到任何答案。

请帮帮我..我有点迷失方向..

谢谢,

我的原始代码(在2.3 api上工作):

在AccountController.cs文件中:

//
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
    {
        return RedirectToAction("Login");
    }
    //A way to get fb details about the log-in user: 
    //var firstNameClaim = loginInfo.ExternalIdentity.Claims.First(c => c.Type == "urn:facebook:first_name");  <--worked only on 2.3
    //var firstNameClaim = loginInfo.ExternalIdentity.Claims.First(c => c.Type == "urn:facebook:name"); <--works on 2.4 api

    // Sign in the user with this external login provider if the user already has a login
    var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
        case SignInStatus.Failure:
        default:
            // If the user does not have an account, then prompt the user to create an account
            ViewBag.ReturnUrl = returnUrl;
            ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
            return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });  //<---DOESN'T WORK. loginInfo.Email IS NULL
    }
}

在Startup.Auth.cs文件中:
    Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions fbOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
    {
        AppId = System.Configuration.ConfigurationManager.AppSettings.Get("FacebookAppId"),
        AppSecret = System.Configuration.ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
    };
    fbOptions.Scope.Add("email");
    fbOptions.Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
    {
        OnAuthenticated = (context) =>
        {
            context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
            foreach (var claim in context.User)
            {
                var claimType = string.Format("urn:facebook:{0}", claim.Key);
                string claimValue = claim.Value.ToString();
                if (!context.Identity.HasClaim(claimType, claimValue))
                    context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, "XmlSchemaString", "Facebook"));

            }
            return System.Threading.Tasks.Task.FromResult(0);
        }
    };
    fbOptions.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
    app.UseFacebookAuthentication(fbOptions);

我刚刚设置了一个新的Facebook应用程序 - 为什么它会显示API版本2.11?!在2017年?! - niico
6个回答

81

参考Katana主题,我设计了以下内容:

更改FacebookAuthenticationOptions以包括下面所示的BackchannelHttpHandlerUserInformationEndpoint。 确保包含您的实现所需的字段名称。

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

然后创建一个自定义的FacebookBackChannelHandler,它将拦截发送到Facebook的请求并根据需要修复格式错误的URL。

更新:基于2017年3月27日的FB API更新,FacebookBackChannelHandler已更新。

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

        var result = await base.SendAsync(request, cancellationToken);
        if (!request.RequestUri.AbsolutePath.Contains("/oauth"))
        {
            return result;
        }

        var content = await result.Content.ReadAsStringAsync();
        var facebookOauthResponse = JsonConvert.DeserializeObject<FacebookOauthResponse>(content);

        var outgoingQueryString = HttpUtility.ParseQueryString(string.Empty);
        outgoingQueryString.Add("access_token", facebookOauthResponse.access_token);
        outgoingQueryString.Add("expires_in", facebookOauthResponse.expires_in + string.Empty);
        outgoingQueryString.Add("token_type", facebookOauthResponse.token_type);
        var postdata = outgoingQueryString.ToString();

        var modifiedResult = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(postdata)
        };

        return modifiedResult;
    }
}

public class FacebookOauthResponse
{
    public string access_token { get; set; }
    public string token_type { get; set; }
    public int expires_in { get; set; }
}

有一个有用的补充是检查库的版本3.0.1并在其更改时抛出异常。这样,您就会知道在修复此问题后,如果有人升级或更新NuGet包,您将会得到通知。

(已更新为可在C# 5中构建和工作,不需要使用新的"nameof"特性)


非常好!工作得很顺利,无需调用额外的 API 来获取电子邮件。 - Douglas Gandini
1
不客气。我也花了整整一天来解决这个问题。发现解决方案后不分享感觉不道德。 - Mike Trionfo
1
仍然无法收到电子邮件。 - Bimal Das
1
只想补充一下,在Facebook API 2.7和Microsoft.Owin.Security.Facebook 3.0.1中仍需要此解决方案。 - roberth
1
在更新3.1.0-rc之后,邮件始终为空,这段代码为我解决了问题。谢谢! - Elger Mensonides
显示剩余12条评论

23

对我来说,这个问题是通过升级到 Microsoft.Owin.Security.Facebook 3.1.0 并将'email'添加到Fields集合中来解决的:

var options = new FacebookAuthenticationOptions
{
    AppId = "-------",
    AppSecret = "------",    
};
options.Scope.Add("public_profile");
options.Scope.Add("email");

//add this for facebook to actually return the email and name
options.Fields.Add("email");
options.Fields.Add("name");

app.UseFacebookAuthentication(options);

1
比其他答案更简单,而且按照描述的方式工作。我很惊讶通常包括的名称必须明确调用。 - JSWilson
2
这似乎是最简单和最好的解决方案。确保添加范围和名称字段。此外,我在添加电子邮件字段之前已经获得了名称字段的返回。如果您需要两个字段,请确保也将其添加进去。 - DrewF
Options.Fields... 返回错误 "FacebookAuthenticationOptions" 不包含字段的定义... - niico
不得不两次请求听起来很糟糕。如其他解决方案中所述,不得不进行后续调用甚至更糟糕。在我看来,这个答案是迄今为止最简单的。 - Giannis Paraskevopoulos
1
要让字段的定义显示出来,请将Microsoft.Owin.Security.Facebook引用更新到3.1.0版本(您可能在使用3.0.1版本)。请执行@niico。 - TomEberhard
显示剩余2条评论

20

要解决这个问题,您需要从NuGet包中安装Facebook SDK。

在StartUp文件中

 app.UseFacebookAuthentication(new FacebookAuthenticationOptions
            {
                AppId = "XXXXXXXXXX",
                AppSecret = "XXXXXXXXXX",
                Scope = { "email" },
                Provider = new FacebookAuthenticationProvider
                {
                    OnAuthenticated = context =>
                    {
                        context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
                        return Task.FromResult(true);
                    }
                }
            });

在控制器或助手中

var identity = AuthenticationManager.GetExternalIdentity(DefaultAuthenticationTypes.ExternalCookie);
var accessToken = identity.FindFirstValue("FacebookAccessToken");
var fb = new FacebookClient(accessToken);
dynamic myInfo = fb.Get("/me?fields=email,first_name,last_name,gender"); // specify the email field

通过这个,你可以获取电子邮件ID、名字和姓氏以及性别。

你也可以在查询字符串中添加其他需要的属性。

希望这能帮助到某些人。


2
谢谢!这也解决了我的问题。如果你想从动态myInfo中获取值,有一个简单的方法:myInfo.email(它可以是email、gender、first_name等)。 - Münich

13

只是想在Mike的回答上补充一点,这行代码

facebookOptions.Scope.Add("email");

之后仍需添加

var facebookOptions = new FacebookAuthenticationOptions()
{
    AppId = "*",
    AppSecret = "*",
    BackchannelHttpHandler = new FacebookBackChannelHandler(),
    UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=id,name,email,first_name,last_name,location"
}

如果您已将 Facebook 帐户注册到 dev 网站,并且未开通“电子邮件权限”,则更改代码并再次尝试后,您仍将无法获取电子邮件,因为电子邮件权限未授予您的 dev 网站。我的做法是前往https://www.facebook.com/settings?tab=applications,删除我的 Facebook 应用程序,然后重新进行该过程。


是的,就像上面所说的一样。这是完整的解决方案。 - Maxim Gershkovich

3

1

请阅读更新日志,这是按设计要求的。您需要在响应中明确请求您想要返回的字段和边缘:

声明性字段 为了尝试提高移动网络上的性能,v2.4中的节点和边缘要求您显式请求您需要的字段。例如,GET /v2.4/me/feed不再默认包括喜欢和评论,但GET /v2.4/me/feed?fields=comments,likes将返回数据。有关更多详细信息,请参见文档,了解如何请求特定字段。


1
是的,我知道,但不知道如何在C#代码中修改外部Facebook OAuth-2中的GET前缀URL。 - Dudi
对我有效的唯一方法是按照这些帖子安装“Facebook SDK for .NET”: [第一个帖子](https://dev59.com/jo3da4cB1Zd3GeqP6OiX), [第二个帖子](https://dev59.com/i1wZ5IYBdhLWcg3wNOCI#31715332)。但是,我希望能够找到不需要安装任何附加包的解决方案。 - Dudi
@Dudi,Mike Trionfo的答案非常好。我只是对它进行了一些改编,以避免固定特定的API版本(UserInformationEndpoint =“https://graph.facebook.com/me?fields=id,name,email”)。 - Douglas Gandini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接