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

时间:2015-01-03 11:10:01

标签: asp.net asp.net-mvc asp.net-web-api asp.net-mvc-5 asp.net-web-api2

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

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

我接下来要知道的是通过api/Account/RegisterExternal标头中的Authorization标记来调用Bearer [Access Token]端点,但这会导致500服务器错误。

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

据我所知,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获取本地令牌并创建该用户身份。

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

2 个答案:

答案 0 :(得分:17)

我错了它接受带有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,如果您只想允许Logged 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);
}

将此帮助方法添加到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)
    };
}

最后,行动使用了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端点用于以两种方式获取令牌,即注册用户并发送本地令牌,或者如果用户已经注册,则发送令牌。

答案 1 :(得分:0)

在所有事情之前,这不是一个完整的答案,这只是答案的一个注释或补充,以避免一些可能花费你几天的问题(在我的情况下为3天) < / p>

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

要解决此问题,您必须将此行代码添加到RegisterExternalToken方法

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