web-dev-qa-db-ja.com

WebApi ASP.NET Identity Facebookログイン

Asp.net IDのfacebook認証フローでは、facebook oauth=ダイアログがアクセストークンではなくコードをredirect_urlに追加するため、サーバーはhttp://localhost:49164/signin-facebook?code=...&state=...を介してこのコードをアクセストークンと交換できます。

私の問題は、私のクライアントがFacebookのSDKを使用するモバイルアプリであり、すぐにアクセストークンを取得することです。 Facebookでは、sdkを使用すると常にアクセストークンが提供されるため、すぐにWeb APIにアクセストークンを提供できますか?

これはあまり安全ではないことを理解していますが、可能ですか?

35
Obi Onuorah

あなたが最終的に解決策を見つけたかどうかはわかりませんが、かなり似たようなことをしようとしていますが、パズルのピースをまとめています。私は実際の解決策を提供していないので、答えではなくコメントとしてこれを投稿しようとしましたが、長すぎます。

どうやらすべてのWebAPI Owin OAuthオプションはブラウザベースです。つまり、ネイティブモバイルアプリに適合しない多くのブラウザリダイレクト要求が必要です(私の場合)。私はまだ調査と実験を行っています。しかし、Hongye Sunのブログ投稿へのコメントの1つで簡単に説明されているように、 http://blogs.msdn.com/b/webdev/archive/2013/09/20/understanding-security-features-in -spa-template.aspx?PageIndex = 2#comments 、Facebookにログインするには、Facebook SDKを使用して受信したアクセストークンを、/ meエンドポイントへのグラフ呼び出しを行うAPIによって直接確認できます。

グラフ呼び出しによって返された情報を使用して、ユーザーが既に登録されているかどうかを確認できます。最後に、おそらくAuthentication.SignIn Owinメソッドを使用してユーザーにサインインし、その後のすべてのAPI呼び出しに使用されるベアラートークンを返す必要があります。

編集:実際に私はそれを間違えました、ベアラートークンは「/ 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エンドポイント応答で見つかった唯一の違いは、ベアラートークンがわずかに短く、有効期限と発行日がGMTではなくUTCであるということです(少なくとも私のマシンでは)。

これがお役に立てば幸いです!

27
s0nica

@ s0nicaの優れたソリューションに続いて、現在実装されているASP.NET MVCテンプレートと統合するためにいくつかのコードを変更しました。 s0nicaアプローチは適切ですが、MVC(Non-WebApi)AccountControllerと完全には互換性がありません。

私のアプローチの利点は、ASP.NET MVCとWebApiの両方で機能することです。

主な違いは、クレーム名です。クレーム名としてFacebookAccessTokenが使用され、その後にリンクが続きます( http://blogs.msdn.com/b/webdev/archive/2013/10/16/get-more-information-from -social-providers-used-in-the-vs-2013-project-templates.aspx )、私のアプローチは特定のリンクのアプローチと互換性があります。私はそれを使用することをお勧めします。

以下のコードは@ 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);
        }
    }

api/AccountController.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;
    }
16
Youngjae

はい、外部アクセストークンを使用して安全にログインできます。

このチュートリアル に従うことを強くお勧めします。これは、Web API 2でトークンベースの認証をゼロから行う方法を示しています(Angular JSをフロントエンドとして使用)。特に、 ステップ4 には、外部アクセストークンを使用して認証できる2つのメソッドが含まれています。たとえば、ネイティブ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 を参照)。

13
Dunc