MVC 5 Web API с токеном доступа Facebook к регистру, без необходимости использования Cookie

Настройка: Новый проект MVC5 с использованием только веб-API. Добавлен Facebook AppId и Secret.

Я могу получить токен для моего веб-API с конечной точки Token, передав в UserName и Password. Затем используйте этот токен для дальнейших вызовов.

НО

Я хочу регистрировать новых пользователей с помощью Facebook SDK в приложении iOS. Я использую Facebook SDK, чтобы получить токен доступа. (Предположим, в этот момент у меня есть токен доступа).

Следующее, что я знаю, - это вызвать конечную точку api/Account/RegisterExternal, передав этот токен в заголовке Authorization с Bearer [Access Token], но это приведет к ошибке 500 серверов.

Я думаю, я знаю причину, Cookie отсутствует. Я сделал тот же звонок с файлом cookie от Fidler, и он сработал. (Cookie получен путем перехода к URL-адресу, предоставленному конечной точкой ExternalLogins). Поскольку cookie отсутствует await Authentication.GetExternalLoginInfoAsync(); внутри действия RegisterExternal возвращает 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();
    }

Я не хочу делать 3 вызова в моем веб-API, чтобы запросить внешние логины, а затем перейти к этому URL-адресу и пройти аутентификацию в токене доступа к веб-обозревателю для доступа к Facebook, а затем вызвать конечную точку RegisterExternal с этим токеном доступа и Cookie, чтобы я необходимо собирать между этими вызовами.

Как я уже сказал, я ничего не менял в шаблоне, кроме идентификаторов Facebook. Тем не менее код выглядит следующим образом.

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");

        }
    }

Насколько я знаю, веб-API не нужен Cookie, и это выглядит правдивым, если у меня есть локальный токен из конечной точки Token, но зачем это нужно для Cookie, в первую очередь при выполнении ExternalRegister Класс 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 для токена. Это Facebook SDK, чтобы получить токен доступа и использовать этот вызов RegisterExternal, чтобы получить локальный токен и создать идентификатор пользователей.

Я сделал домашнее задание, и я застрял в этой мысли. Мысли оценили!

Ответ 1

Я ошибся, что он принимает Social Token с печеньем! Он не принимает внешний токен напрямую.

Дело в том, что MVC 5 заботится обо всем для нас, т.е. собирает токен из социальных медиа и проверяет/обрабатывает его. После этого он генерирует локальный токен.

Метод RegisterExternal также требует, чтобы файлы cookie поддерживались, решение не работает.

Я написал сообщение в блоге, которое объясняется в detail. Добавлен прямой ответ ниже. Я стремился сделать его смешением и почувствовать неотъемлемую часть потока входа/регистрации стандартного MVC Web API, чтобы убедиться, что его легко понять.

После решения ниже атрибут Authorize должен работать следующим образом: вы получите неавторизованный ответ.

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

Используйте 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);
}

Добавьте этот вспомогательный метод в класс 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")
    {
        // not implemented yet
        return null;
        //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; }
}

Да, мы передаем электронное письмо вместе с данными Token во время регистрации, это не приведет к изменению кода при использовании Twitter, поскольку Twitter не предоставляет пользователям электронную почту. Мы подтверждаем токен из нашего приложения. После того как электронная почта зарегистрирована, взломанный или какой-либо другой токен не может использоваться для изменения электронной почты или получения локального токена для этого письма, поскольку он всегда будет возвращать локальный токен для фактического пользователя Social Token, переданного независимо от отправленного сообщения.

RegisterExternalToken Конечная точка работает, чтобы получить токен в обоих направлениях, то есть зарегистрировать пользователя и отправить локальный токен, или если пользователь уже зарегистрирован, а затем отправить токен.