ASP.NET Web API和身份识别与Facebook登录

问题描述 投票:36回答:3

在ASP.NET Identity的Facebook身份验证流程中,Facebook OAuth对话框向 redirect_url 这样服务器就可以通过例如......等方式将此代码换成访问令牌。

http://localhost:49164/signin-facebook?code=…&state=…

我的问题是,我的客户端是一个使用Facebook SDK的移动应用,它直接给我一个访问令牌。Facebook说使用SDK 始终 给你一个访问令牌,那么我可以直接把这个令牌给ASP.NET Web API吗?

我知道这样做不是很安全,但这有可能吗?

facebook asp.net-web-api asp.net-identity facebook-android-sdk facebook-ios-sdk
3个回答
29
投票

我不知道你是否找到了解决方案,但我正在尝试做类似的事情,我仍然在把拼图的碎片放在一起。我曾试图把这个作为评论而不是答案发布,因为我没有提供一个真正的解决方案,但它太长了。

显然,所有的WebAPI Owin OAuth选项都是基于浏览器的--也就是说,它们需要大量的浏览器重定向请求,而这些重定向请求并不适合原生移动应用(如我的情况所需)。我还在调查和实验,但由于 孙宏业在其博客文章的评论中简要介绍了如果要登录Facebook,使用Facebook SDK收到的访问令牌可以直接通过API调用图调用 /me 端点。

通过使用图调用返回的信息,你就可以检查用户是否已经注册。最后,我们需要对用户进行登录,也许使用Owin的 Authentication.SignIn 方法,返回一个将用于所有后续API调用的承载令牌。

EDIT: 实际上,我弄错了 不记名标记是在调用 /Token 端点,输入时接受类似于

grant_type=password&username=Alice&password=password123

这里的问题是我们没有密码--这是OAuth机制的重点--所以我们还能怎么调用 "密码"?/Token 端点?

更新。 我终于找到了一个可行的解决方案,以下是我必须在现有的类中添加的内容,以使其工作。

Startup.Auth.cs

public partial class Startup
{
    /// <summary>
    /// This part has been added to have an API endpoint to authenticate users that accept a Facebook access token
    /// </summary>
    static Startup()
    {
        PublicClientId = "self";

        //UserManagerFactory = () => new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
        UserManagerFactory = () => 
        {
            var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
            userManager.UserValidator = new UserValidator<ApplicationUser>(userManager) { AllowOnlyAlphanumericUserNames = false };
            return userManager;
        };

        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
            AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            AllowInsecureHttp = true
        };

        OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
        OAuthBearerOptions.AccessTokenFormat = OAuthOptions.AccessTokenFormat;
        OAuthBearerOptions.AccessTokenProvider = OAuthOptions.AccessTokenProvider;
        OAuthBearerOptions.AuthenticationMode = OAuthOptions.AuthenticationMode;
        OAuthBearerOptions.AuthenticationType = OAuthOptions.AuthenticationType;
        OAuthBearerOptions.Description = OAuthOptions.Description;
        OAuthBearerOptions.Provider = new CustomBearerAuthenticationProvider();            
        OAuthBearerOptions.SystemClock = OAuthOptions.SystemClock;
    }

    public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }

    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

    public static Func<UserManager<ApplicationUser>> UserManagerFactory { get; 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)
    {
        [Initial boilerplate code]

        OAuthBearerAuthenticationExtensions.UseOAuthBearerAuthentication(app, OAuthBearerOptions);

        [More boilerplate code]
    }
}

public class CustomBearerAuthenticationProvider : OAuthBearerAuthenticationProvider
{
    public override Task ValidateIdentity(OAuthValidateIdentityContext context)
    {
        var claims = context.Ticket.Identity.Claims;
        if (claims.Count() == 0 || claims.Any(claim => claim.Issuer != "Facebook" && claim.Issuer != "LOCAL_AUTHORITY" ))
            context.Rejected();
        return Task.FromResult<object>(null);
    }
}

而在 AccountController我添加了以下动作。

[HttpPost]
[AllowAnonymous]
[Route("FacebookLogin")]
public async Task<IHttpActionResult> FacebookLogin(string token)
{
    [Code to validate input...]
    var tokenExpirationTimeSpan = TimeSpan.FromDays(14);            
    ApplicationUser user = null;    
    // Get the fb access token and make a graph call to the /me endpoint    
    // Check if the user is already registered
    // If yes retrieve the user 
    // If not, register it  
    // Finally sign-in the user: this is the key part of the code that creates the bearer token and authenticate the user
    var identity = new ClaimsIdentity(Startup.OAuthBearerOptions.AuthenticationType);
    identity.AddClaim(new Claim(ClaimTypes.Name, user.Id, null, "Facebook"));
        // This claim is used to correctly populate user id
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id, null, "LOCAL_AUTHORITY"));
    AuthenticationTicket ticket = new AuthenticationTicket(identity, new AuthenticationProperties());            
    var currentUtc = new Microsoft.Owin.Infrastructure.SystemClock().UtcNow;
    ticket.Properties.IssuedUtc = currentUtc;
    ticket.Properties.ExpiresUtc = currentUtc.Add(tokenExpirationTimeSpan);            
    var accesstoken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket); 
    Authentication.SignIn(identity);

    // Create the response
    JObject blob = new JObject(
        new JProperty("userName", user.UserName),
        new JProperty("access_token", accesstoken),
        new JProperty("token_type", "bearer"),
        new JProperty("expires_in", tokenExpirationTimeSpan.TotalSeconds.ToString()),
        new JProperty(".issued", ticket.Properties.IssuedUtc.ToString()),
        new JProperty(".expires", ticket.Properties.ExpiresUtc.ToString())
    );
    var json = Newtonsoft.Json.JsonConvert.SerializeObject(blob);
    // Return OK
    return Ok(blob);
}

就是这样!我发现与经典的 /Token 端点的响应是,不记名令牌的时间稍短,到期和发行日期是以 UTC 为单位,而不是以 GMT 为单位(至少在我的机器上是这样)。

我希望这能帮助你


16
投票

按照 @s0nica 的伟大解决方案,我修改了一些代码,以便与当前实现的 ASP.NET MVC 模板集成。s0nica 的方法很好,但它与 MVC(Non-WebApi)不完全兼容。AccountController.

我的方法的好处是,它的工作与 两者 ASP.NET MVC ASP.NET Web API。

最主要的区别是索赔名称。由于要求的名称是 FacebookAccessToken 是用 在这个MSDN博客上我的方法和链接中给出的方法是兼容的。我建议用这个方法。

请注意,下面的代码是@s0nica的答案的修改版。所以,(1)先看上面的链接,然后(2)再看@s0nica的代码,最后(3)再考虑我的代码。

Startup.Auth.cs文件。

public class CustomBearerAuthenticationProvider : OAuthBearerAuthenticationProvider
{
    // This validates the identity based on the issuer of the claim.
    // The issuer is set in the API endpoint that logs the user in
    public override Task ValidateIdentity(OAuthValidateIdentityContext context)
    {
        var claims = context.Ticket.Identity.Claims;
        if (!claims.Any() || claims.Any(claim => claim.Type != "FacebookAccessToken")) // modify claim name
            context.Rejected();
        return Task.FromResult<object>(null);
    }
}

apiAccountController.cs

// POST api/Account/FacebookLogin
[HttpPost]
[AllowAnonymous]
[Route("FacebookLogin")]
public async Task<IHttpActionResult> FacebookLogin([FromBody] FacebookLoginModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (string.IsNullOrEmpty(model.token))
    {
        return BadRequest("No access token");
    }

    var tokenExpirationTimeSpan = TimeSpan.FromDays(300);
    ApplicationUser user = null;
    string username;
    // Get the fb access token and make a graph call to the /me endpoint
    var fbUser = await VerifyFacebookAccessToken(model.token);
    if (fbUser == null)
    {
        return BadRequest("Invalid OAuth access token");
    }

    UserLoginInfo loginInfo = new UserLoginInfo("Facebook", model.userid);
    user = await UserManager.FindAsync(loginInfo);

    // If user not found, register him with username.
    if (user == null)
    {
        if (String.IsNullOrEmpty(model.username))
            return BadRequest("unregistered user");

        user = new ApplicationUser { UserName = model.username };

        var result = await UserManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await UserManager.AddLoginAsync(user.Id, loginInfo);
            username = model.username;
            if (!result.Succeeded)
                return BadRequest("cannot add facebook login");
        }
        else
        {
            return BadRequest("cannot create user");
        }
    }
    else
    {
        // existed user.
        username = user.UserName;
    }

    // common process: Facebook claims update, Login token generation
    user = await UserManager.FindByNameAsync(username);

    // Optional: make email address confirmed when user is logged in from Facebook.
    user.Email = fbUser.email;
    user.EmailConfirmed = true;
    await UserManager.UpdateAsync(user);

    // Sign-in the user using the OWIN flow
    var identity = new ClaimsIdentity(Startup.OAuthBearerOptions.AuthenticationType);

    var claims = await UserManager.GetClaimsAsync(user.Id);
    var newClaim = new Claim("FacebookAccessToken", model.token); // For compatibility with ASP.NET MVC AccountController
    var oldClaim = claims.FirstOrDefault(c => c.Type.Equals("FacebookAccessToken"));
    if (oldClaim == null)
    {
        var claimResult = await UserManager.AddClaimAsync(user.Id, newClaim);
        if (!claimResult.Succeeded)
            return BadRequest("cannot add claims");
    }
    else
    {
        await UserManager.RemoveClaimAsync(user.Id, oldClaim);
        await UserManager.AddClaimAsync(user.Id, newClaim);
    }

    AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
    var currentUtc = new Microsoft.Owin.Infrastructure.SystemClock().UtcNow;
    properties.IssuedUtc = currentUtc;
    properties.ExpiresUtc = currentUtc.Add(tokenExpirationTimeSpan);
    AuthenticationTicket ticket = new AuthenticationTicket(identity, properties);
    var accesstoken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);
    Request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accesstoken);
    Authentication.SignIn(identity);

    // Create the response building a JSON object that mimics exactly the one issued by the default /Token endpoint
    JObject blob = new JObject(
        new JProperty("userName", user.UserName),
        new JProperty("access_token", accesstoken),
        new JProperty("token_type", "bearer"),
        new JProperty("expires_in", tokenExpirationTimeSpan.TotalSeconds.ToString()),
        new JProperty(".issued", ticket.Properties.IssuedUtc.ToString()),
        new JProperty(".expires", ticket.Properties.ExpiresUtc.ToString()),
        new JProperty("model.token", model.token),
    );
    // Return OK
    return Ok(blob);
}

Facebook登录模型的绑定(内部类的 api/AccountController.cs)

public class FacebookLoginModel
{
    public string token { get; set; }
    public string username { get; set; }
    public string userid { get; set; }
}

public class FacebookUserViewModel
{
    public string id { get; set; }
    public string first_name { get; set; }
    public string last_name { get; set; }
    public string username { get; set; }
    public string email { get; set; }
}

VerifyFacebookAccessToken方法(在 api/AccountController.cs)

private async Task<FacebookUserViewModel> VerifyFacebookAccessToken(string accessToken)
{
    FacebookUserViewModel fbUser = null;
    var path = "https://graph.facebook.com/me?access_token=" + accessToken;
    var client = new HttpClient();
    var uri = new Uri(path);
    var response = await client.GetAsync(uri);
    if (response.IsSuccessStatusCode)
    {
        var content = await response.Content.ReadAsStringAsync();
        fbUser = Newtonsoft.Json.JsonConvert.DeserializeObject<FacebookUserViewModel>(content);
    }
    return fbUser;
}

13
投票

是的,你可以使用外部访问令牌来安全登录。

我强烈建议你遵循 本教程该书向你展示了如何从头开始使用Web API 2进行基于令牌的认证(使用Angular JS作为前端)。 特别是: 第四步 包括两个方法,允许您使用外部访问令牌进行身份验证,例如从本地SDK返回的令牌。

[AllowAnonymous, HttpGet]
async Task<IHttpActionResult> ObtainLocalAccessToken(string provider, string externalAccessToken)

[AllowAnonymous, HttpPost]
async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)

简而言之:

  1. 使用本地SDK获取外部访问令牌。

  2. 调用 ObtainLocalAccessToken("Facebook", "[fb-access-token]") 来确定用户是否已经有一个账户(200响应),在这种情况下,一个新的 本地令牌 将为您生成。 它还会验证外部访问令牌是否合法。

  3. 如果步骤2中的呼叫失败(400响应),您需要通过调用 RegisterExternal,传递外部令牌。 上面的教程有一个很好的例子(见 associateController.js).

© www.soinside.com 2019 - 2024. All rights reserved.