WebAPI ASP.NET Identidad Facebook login


En el flujo de autenticación de facebook para asp.net identidad, el diálogo de facebook oauth añade un código en lugar de un token de acceso a la redirect_url para que el servidor pueda intercambiar este código por un token de acceso a través de http://localhost:49164/signin-facebook?code=...&state=....

Mi problema es que mi cliente es una aplicación móvil que utiliza el sdk de Facebook y que de inmediato me da un token de acceso. Facebook dice que el uso del sdk siempre te da un token de acceso, así que ¿puedo darle de inmediato a web api el token de acceso?

Entiendo esto no es muy seguro, pero ¿es posible?

Author: Bruno Bieri, 2014-01-13

3 answers

No se si finalmente encontraste una solución, pero estoy tratando de hacer algo bastante similar y todavía estoy juntando las piezas del rompecabezas. Había intentado publicar esto como un comentario en lugar de una respuesta, ya que no proporciono una solución real, pero es demasiado largo.

Aparentemente todas las opciones de WebAPI Owin OAuth están basadas en el navegador, es decir, requieren un montón de solicitudes de redirecciones del navegador que no se ajustan a una aplicación móvil nativa (mi caso). Todavía estoy investigando y experimentando, pero como describió brevemente Hongye Sun en uno de los comentarios a su entrada de blog, http://blogs.msdn.com/b/webdev/archive/2013/09/20/understanding-security-features-in-spa-template.aspx?PageIndex=2#comments, para iniciar sesión con Facebook, el token de acceso recibido usando Facebook SDK puede ser verificado directamente por la API haciendo una llamada gráfica al punto final / me.

Utilizando la información devuelta por la llamada a graph, puede comprobar si el usuario ya está registrado o no. Al final tenemos que inicie sesión en el usuario, tal vez usando autenticación.SignIn método Owin, devolver un token portador que se utilizará para todas las llamadas API posteriores.

EDITAR: En realidad me equivoqué, el token portador se emite al llamar al punto final "/ Token", que en la entrada acepta algo como grant_type=password&username=Alice&password=password123 El problema aquí es que no tenemos una contraseña (ese es todo el punto del mecanismo OAuth), así que ¿de qué otra manera podemos invocar el punto final "/Token"?

ACTUALIZAR: Finalmente encontré una solución de trabajo y el lo siguiente es lo que tuve que agregar a las clases existentes para que funcionara: Inicio.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);
    }
}

En AccountController he añadido la siguiente acción

        [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);
        }

Eso es todo. La única diferencia que encontré con la respuesta de punto final classic /Token es que el token portador es ligeramente más corto y las fechas de vencimiento y emisión están en UTC en lugar de en GMT (al menos en mi máquina).

Espero que esto ayude!

 25
Author: s0nica,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-10 09:42:31

Seguido de una gran solución de @s0nica, modificé algunos códigos para integrarlos con los implementados actualmente ASP.NET Plantilla MVC. el enfoque de s0nica es bueno, pero no es totalmente compatible con MVC (Non-WebAPI) AccountController.

El beneficio de mi enfoque es trabajar con ambos ASP.NET MVC y WebAPI viceversa.

Las principales diferencias son el nombre de la reclamación. Como nombre de la reclamación FacebookAccessToken se utiliza como seguido por el enlace ( http://blogs.msdn.com/b/webdev/archive/2013/10/16/get-more-information-from-social-providers-used-in-the-vs-2013-project-templates.aspx), mi enfoque es compatible con el enfoque del enlace dado. Recomiendo usarlo con él.

Tenga en cuenta que los siguientes códigos son una versión modificada de la respuesta de @s0nica. Por lo tanto, (1) recorrido por el enlace dado, (2) y luego recorrido por el código de s0nica, (3) y finalmente considere el mío después.

Inicio.Auth.cs file.

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

Modelo de inicio de sesión de Facebook para el enlace (clase interna de 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; }
    }

Método VerifyFacebookAccessToken (en 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;
    }
 15
Author: Youngjae,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-03 04:04:16

Sí, puede usar un token de acceso externo para iniciar sesión de forma segura.

Le recomiendo encarecidamente que siga este tutorial, que le muestra cómo realizar la autenticación basada en tokens con Web API 2 desde cero (utilizando Angular JS como front-end). En particular, el paso 4 incluye dos métodos que le permiten autenticarse usando un token de acceso externo, por ejemplo, como devuelto desde un SDK nativo:

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

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

En pocas palabras:

  1. Usar SDK nativo para obtener datos externos token de acceso.

  2. Llame a ObtainLocalAccessToken("Facebook", "[fb-access-token]") para determinar si el usuario ya tiene una cuenta (respuesta 200), en cuyo caso se generará un nuevo token local para usted. También verifica que el token de acceso externo sea legítimo.

  3. Si la llamada en el paso 2 falló (respuesta 400), debe registrar una nueva cuenta llamando a RegisterExternal, pasando el token externo. El tutorial anterior tiene un buen ejemplo de esto (ver Controlador asociado.js ).

 13
Author: Dunc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-20 13:50:03