带有 Facebook 访问令牌的 MVC 5 Web API 到 RegisterExternal,无需 Cookie

Posted

技术标签:

【中文标题】带有 Facebook 访问令牌的 MVC 5 Web API 到 RegisterExternal,无需 Cookie【英文标题】:MVC 5 Web API with Facebook access token to RegisterExternal without need of Cookie 【发布时间】:2015-03-01 11:51:20 【问题描述】:

设置: 仅使用 Web API 的新 MVC5 项目。添加了 Facebook AppId 和 Secret。 我可以通过传入用户名和密码从Token 端点获取我的 Web API 的令牌。然后使用该令牌进行进一步调用。

但是 我想在 ios 应用程序中借助 Facebook SDK 注册新用户。 我正在使用 Facebook SDK 获取访问令牌。 (假设此时,我有一个访问令牌)。

我知道的下一件事是调用api/Account/RegisterExternal 端点,方法是通过在Authorization 标头中使用Bearer [Access Token] 传递这个令牌,但这会导致500 服务器错误。

我想我知道原因了,Cookie 不见了。我用 Fidler 的 cookie 打了同样的电话,它奏效了。 (通过转到ExternalLogins 端点提供的 URL 来接收 Cookie)。 由于在 RegisterExternal 操作中缺少 await Authentication.GetExternalLoginInfoAsync(); cookie,因此返回 null。

// POST api/Account/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("RegisterExternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)

    if (!ModelState.IsValid)
    
        return BadRequest(ModelState);
    

    var info = await Authentication.GetExternalLoginInfoAsync();
    if (info == null)
    
        return InternalServerError();
    

    var user = new ApplicationUser()  UserName = model.Email, Email = model.Email ;

    IdentityResult result = await UserManager.CreateAsync(user);
    if (!result.Succeeded)
    
        return GetErrorResult(result);
    

    result = await UserManager.AddLoginAsync(user.Id, info.Login);
    if (!result.Succeeded)
    
        return GetErrorResult(result);
    
    return Ok();

我不想对我的 Web API 进行 3 次调用以请求外部登录,然后转到该 URL 并在 Web 浏览器中对 Facebook 访问令牌进行身份验证,然后使用该访问令牌和我的 Cookie 调用 RegisterExternal 端点需要在这些调用之间收集。

正如我所说,除了 Facebook Id,我没有更改模板中的任何内容。代码如下。

public partial class Startup

    public static OAuthAuthorizationServerOptions OAuthOptions  get; private set; 

    public static string PublicClientId  get; private set; 

    // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    
        // Configure the db context and user manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Configure the application for OAuth based flow
        PublicClientId = "self";
        OAuthOptions = new OAuthAuthorizationServerOptions
        
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(PublicClientId),
            AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            AllowInsecureHttp = true
        ;

        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);

        app.UseFacebookAuthentication(
            appId: "xxxxxxxxxxxxxxx",
            appSecret: "xxxxxxxxxxxxxxxxxxxxxxxx");
    

据我所知,Web API 不需要 Cookie,当我从 Token 端点获得本地令牌时,这似乎是正确的,但为什么在执行 ExternalRegister 时首先需要 Cookie WebApiConfig 类看起来像这样,config.SuppressDefaultHostAuthentication(); 不应该避免任何 Cookie 需求

public static class WebApiConfig

    public static void Register(HttpConfiguration config)
    
        // Web API configuration and services
        // Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/controller/id",
            defaults: new  id = RouteParameter.Optional 
        );
    

我不知道我是否错过了这里的要点。我的意图是不需要在本机 iOS 应用程序中使用 Web 浏览器来获取令牌。那是 Facebook SDK 获取访问令牌并使用该调用 RegisterExternal 获取本地令牌并创建该用户身份。

我做了功课,我坚持这个想法。 想法表示赞赏!

【问题讨论】:

找到任何解决方案了吗?我会很感兴趣,因为我有同样的问题。 @Freddy 是的,我最终将不同的部分放在一起创建自己的解决方案,效果很好。将发布它。目前不在城里,如果我不发布,请在 4 天后 ping 我。 你真是太慷慨了。我目前也在创建自己的解决方案...... @Freddy 在下面找到答案 :) 我希望它有所帮助。 【参考方案1】:

我错了,它接受带有 cookie 的社交令牌! 它不直接接受任何外部令牌。

问题是.. MVC 5 为我们处理一切,即从社交媒体收集令牌并验证/处理它。之后它会生成一个本地令牌。

RegisterExternal 方法也需要维护 cookie,解决方案不需要。

我已经写了一个blog post,它将详细说明。在下面添加了直截了当的答案。我的目标是让它融入默认 MVC Web API 的登录/注册流程并使其成为不可或缺的一部分,以确保其易于理解。

在以下解决方案之后,授权属性必须如下所示才能工作,否则您将得到未经授权的响应。

[Authorize]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalBearer)]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ApplicationCookie)]

如果您希望仅允许令牌使用 API,请使用 ExternalBearer,如果您希望仅允许记录的 cookie 使用 API(即来自网站),请使用 ApplicationCookie。如果您想为两者都允许 API,请同时使用两者。

将此操作添加到AccountController.cs

// POST api/Account/RegisterExternalToken
[OverrideAuthentication]
[AllowAnonymous]
[Route("RegisterExternalToken")]
public async Task<IHttpActionResult> RegisterExternalToken(RegisterExternalTokenBindingModel model)

    if (!ModelState.IsValid)
    
        return BadRequest(ModelState);
    

    ExternalLoginData externalLogin = await ExternalLoginData.FromToken(model.Provider, model.Token);

    if (externalLogin == null)
    
        return InternalServerError();
    

    if (externalLogin.LoginProvider != model.Provider)
    
        Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
        return InternalServerError();
    

    ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
        externalLogin.ProviderKey));

    bool hasRegistered = user != null;
    ClaimsIdentity identity = null;
    IdentityResult result;

    if (hasRegistered)
    
        identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
        IEnumerable<Claim> claims = externalLogin.GetClaims();
        identity.AddClaims(claims);
        Authentication.SignIn(identity);
    
    else
    
        user = new ApplicationUser()  Id = Guid.NewGuid().ToString(), UserName = model.Email, Email = model.Email ;

        result = await UserManager.CreateAsync(user);
        if (!result.Succeeded)
        
            return GetErrorResult(result);
        

        var info = new ExternalLoginInfo()
        
            DefaultUserName = model.Email,
            Login = new UserLoginInfo(model.Provider, externalLogin.ProviderKey)
        ;

        result = await UserManager.AddLoginAsync(user.Id, info.Login);
        if (!result.Succeeded)
        
            return GetErrorResult(result);
        

        identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
        IEnumerable<Claim> claims = externalLogin.GetClaims();
        identity.AddClaims(claims);
        Authentication.SignIn(identity);
    

    AuthenticationTicket ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
    var currentUtc = new Microsoft.Owin.Infrastructure.SystemClock().UtcNow;
    ticket.Properties.IssuedUtc = currentUtc;
    ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromDays(365));
    var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
    Request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

    // Create the response building a JSON object that mimics exactly the one issued by the default /Token endpoint
    JObject token = new JObject(
        new JProperty("userName", user.UserName),
        new JProperty("id", user.Id),
        new JProperty("access_token", accessToken),
        new JProperty("token_type", "bearer"),
        new JProperty("expires_in", TimeSpan.FromDays(365).TotalSeconds.ToString()),
        new JProperty(".issued", currentUtc.ToString("ddd, dd MMM yyyy HH':'mm':'ss 'GMT'")),
        new JProperty(".expires", currentUtc.Add(TimeSpan.FromDays(365)).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'"))
    );
    return Ok(token);

将此辅助方法添加到AccountController.cs 的辅助区域中的ExternalLoginData 类中

public static async Task<ExternalLoginData> FromToken(string provider, string accessToken)

    string verifyTokenEndPoint = "", verifyAppEndpoint = "";

    if (provider == "Facebook")
    
        verifyTokenEndPoint = string.Format("https://graph.facebook.com/me?access_token=0", accessToken);
        verifyAppEndpoint = string.Format("https://graph.facebook.com/app?access_token=0", accessToken);
    
    else if (provider == "Google")
    
        return null; // not implemented yet
        //verifyTokenEndPoint = string.Format("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=0", accessToken);
    
    else
    
        return null;
    

    HttpClient client = new HttpClient();
    Uri uri = new Uri(verifyTokenEndPoint);
    HttpResponseMessage response = await client.GetAsync(uri);
    ClaimsIdentity identity = null;
    if (response.IsSuccessStatusCode)
    
        string content = await response.Content.ReadAsStringAsync();
        dynamic iObj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);

        uri = new Uri(verifyAppEndpoint);
        response = await client.GetAsync(uri);
        content = await response.Content.ReadAsStringAsync();
        dynamic appObj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);

        identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);

        if (provider == "Facebook")
        
            if (appObj["id"] != Startup.facebookAuthOptions.AppId)
            
                return null;
            

            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, iObj["id"].ToString(), ClaimValueTypes.String, "Facebook", "Facebook"));

        
        else if (provider == "Google")
        
            //not implemented yet
        
    

    if (identity == null)
        return null;

    Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);

    if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer) || String.IsNullOrEmpty(providerKeyClaim.Value))
        return null;

    if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
        return null;

    return new ExternalLoginData
    
        LoginProvider = providerKeyClaim.Issuer,
        ProviderKey = providerKeyClaim.Value,
        UserName = identity.FindFirstValue(ClaimTypes.Name)
    ;

最后,RegisterExternalTokenBindingModel 被操作使用。

public class RegisterExternalTokenBindingModel

    [Required]
    [Display(Name = "Email")]
    public string Email  get; set; 

    [Required]
    [Display(Name = "Token")]
    public string Token  get; set; 

    [Required]
    [Display(Name = "Provider")]
    public string Provider  get; set; 

是的,我们在注册时将电子邮件与令牌详细信息一起传递,这不会导致您在使用 Twitter 时更改代码,因为 Twitter 不向用户提供电子邮件。我们验证令牌来自我们的应用程序。一旦电子邮件注册、被黑或其他人的令牌不能用于更改电子邮件或获取该电子邮件的本地令牌,因为无论发送的电子邮件如何,它都会始终为传递的社交令牌的实际用户返回本地令牌。

RegisterExternalToken 端点以两种方式获取令牌,即注册用户并发送本地令牌,或者如果用户已经注册,则发送令牌。

【讨论】:

哇,这太棒了——干得好!虽然我已经通过使用 2 个自己的端点(一个用于注册,一个用于登录并将第 3 方 ID 放在我自己的用户表中)实现了自己的解决方案,但我很快就会尝试一下。因为我已经集成了 twitter,所以我可能不得不调整你的解决方案,因为 twitter 使用 appkey、appsecret 以及 usertoken 和 usertokensecret 来验证......你怎么看?还有一点是你在哪里配置社交应用密钥等等?在带有 app.useFacebookAuth 的 StartupClass 中? @Freddy 您的安全问题已解决。还改进了代码。如果您在代码中发现任何其他内容,请告诉我。 :) @KevalPatel 您应该在一个应用程序的任何地方使用相同的 ID。无论如何,用下面的替换它,你应该很高兴。 if (appObj["id"] != Startup.facebookAuthOptions.AppId || appObj["id"] != "your other id here") return null; 你能给我们提供类“ExternalLoginData”类吗? @CiaranGallagher 已备份,忘记更新域名了,唷!很接近了。是的,这对你的问题有用,我会在那里回答。【参考方案2】:

首先,这不是一个完整的答案,这只是一个注释或答案的补充,以避免一些可能花费您几天(在我的情况下为 3 天)的问题

上一个答案是它只是缺少一件事的完整答案,如下: 如果您为 Authorize 属性指定了 role,例如 [Authorize("UserRole")] ,则之前的设置仍然会给您 401 错误,因为解决方案没有设置 RoleClaim

要解决这个问题,你必须将这行代码添加到RegisterExternalToken 方法中

oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, "UserRole"));

【讨论】:

以上是关于带有 Facebook 访问令牌的 MVC 5 Web API 到 RegisterExternal,无需 Cookie的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.Net MVC 应用程序中从 Facebook SDK for .Net 获取 Facebook 用户访问令牌

使用 Facebook 的访问令牌在 UIWebView 中显示带有经过身份验证的用户会话的粉丝页面

ASP.NET MVC - 将数据发布到 Facebook

注销后facebook访问令牌仍然有效

在 Facebook iOS SDK 3.5.x 中获取 Facebook 页面访问令牌

将 Facebook 访问令牌传递到评论框