具有Facebook访问令牌的MVC 5 Web API到RegisterExternal而不需要Cookie

par*_*tkr 12 asp.net asp.net-mvc asp.net-web-api asp.net-mvc-5 asp.net-web-api2

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

我想在iOS应用程序的Facebook SDK的帮助下注册新用户.我正在使用Facebook SDK来获取访问令牌.(假设此时我有一个访问令牌).

我知道的下一件事是api/Account/RegisterExternal通过在Authorization标头中传递此标记来调用端点,Bearer [Access Token]但这导致500服务器错误.

我想我知道原因,Cookie丢失了.我用Fidler的一个cookie做了同样的电话,但它确实奏效了.(通过转到ExternalLogins端点提供的URL来接收Cookie ).由于await Authentication.GetExternalLoginInfoAsync();RegisterExternal操作中缺少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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

据我所知,Web API不需要Cookie,当我从Token端点获得本地令牌时,它看起来是正确的,但是为什么在做ExternalRegister WebApiConfig类时,它首先需要Cookie,并且不应该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 }
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我完成了我的作业,我坚持这个想法.赞赏的想法!

par*_*tkr 17

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

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

RegisterExternal方法还需要维护cookie,而解决方案则不需要.

我写了一篇博文,详细解释.在下面添加了直截了当的答案.我的目标是使它融合并感觉默认MVC Web API的登录/注册流程的组成部分,以确保它易于理解.

在以下解决方案之后,Authorize属性必须如下工作,否则您将获得未经授权的响应.

[Authorize]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalBearer)]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ApplicationCookie)]
Run Code Online (Sandbox Code Playgroud)

使用ExternalBearer,如果你希望只允许令牌使用API,使用ApplicationCookie,如果你希望只允许登录cookie的使用API,即从一个网站.用户如果要同时允许两者的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);
}
Run Code Online (Sandbox Code Playgroud)

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

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)
    };
}
Run Code Online (Sandbox Code Playgroud)

最后,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; }
}
Run Code Online (Sandbox Code Playgroud)

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

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