web-dev-qa-db-fra.com

Connexion Facebook à l'identité WebApi ASP.NET

Dans le flux d'authentification facebook pour l'identité asp.net, la boîte de dialogue facebook oauth ajoute un code plutôt qu'un jeton d'accès à redirect_url afin que le serveur puisse échanger ce code contre un jeton d'accès via http://localhost:49164/signin-facebook?code=...&state=....

Mon problème est que mon client est une application mobile qui utilise le facebook sdk et qui me donne immédiatement un jeton d'accès. Facebook dit que l'utilisation du sdk vous donne toujours un jeton d'accès, alors puis-je tout de suite donner à web api le jeton d'accès?

Je comprends que ce n'est pas très sûr, mais est-ce même possible?

35
Obi Onuorah

Je ne sais pas si vous avez finalement trouvé une solution, mais j'essaie de faire quelque chose d'assez similaire et je suis toujours en train de rassembler les pièces du puzzle. J'avais essayé de poster ceci comme un commentaire au lieu d'une réponse, car je n'apporte pas de vraie solution, mais c'est trop long.

Apparemment, toutes les options WebAPI Owin OAuth sont basées sur un navigateur, c'est-à-dire qu'elles nécessitent de nombreuses demandes de redirection de navigateur qui ne correspondent pas à une application mobile native (mon cas). J'étudie et expérimente toujours, mais comme brièvement décrit par Hongye Sun dans l'un des commentaires de son article de blog, http://blogs.msdn.com/b/webdev/archive/2013/09/20/understanding-security-features-in -spa-template.aspx? PageIndex = 2 # commentaires , pour vous connecter avec Facebook, le jeton d'accès reçu à l'aide du SDK Facebook peut être vérifié directement par l'API qui effectue un appel graphique au point de terminaison/me.

En utilisant les informations renvoyées par l'appel graphique, vous pouvez vérifier si l'utilisateur est déjà enregistré ou non. À la fin, nous devons nous connecter à l'utilisateur, en utilisant peut-être la méthode Authentication.SignIn Owin, en renvoyant un jeton de support qui sera utilisé pour tous les appels d'API suivants.

EDIT: En fait, je me suis trompé, le jeton du porteur est émis lors de l'appel du point de terminaison "/ Token", qui en entrée accepte quelque chose comme grant_type=password&username=Alice&password=password123 Le problème ici est que nous n'avons pas de mot de passe (c'est tout l'intérêt du mécanisme OAuth), alors comment invoquer le point de terminaison "/ Token" autrement?

MISE À JOUR: J'ai finalement trouvé une solution de travail et voici ce que j'ai dû ajouter aux classes existantes pour le faire fonctionner: 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);
    }
}

Dans AccountController, j'ai ajouté l'action suivante

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

C'est ça. La seule différence que j'ai trouvée avec la réponse de point de terminaison classique/jeton est que le jeton du porteur est légèrement plus court et que les dates d'expiration et d'émission sont en UTC plutôt qu'en GMT (au moins sur ma machine).

J'espère que ça aide!

27
s0nica

Suivi par une excellente solution de @ s0nica, j'ai modifié certains codes afin de les intégrer au modèle MVC ASP.NET actuellement implémenté. L'approche s0nica est bonne mais n'est pas entièrement compatible avec MVC (Non-WebApi) AccountController.

L'avantage de mon approche est de travailler avec ASP.NET MVC et WebApi vice-versa.

Les principales différences sont le nom de la revendication. Comme nom de revendication FacebookAccessToken est utilisé suivi du lien ( http://blogs.msdn.com/b/webdev/archive/2013/10/16/get-more-information-from -social-providers-used-in-the-vs-2013-project-templates.aspx ), mon approche est compatible avec l'approche du lien donné. Je recommande de l'utiliser avec.

Notez que les codes ci-dessous sont une version modifiée de la réponse de @ s0nica. Donc, (1) pas à pas le lien donné, (2) puis le code de s0nica pas à pas, (3) et enfin considérer le mien après.

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

Modèle de connexion Facebook pour la liaison (classe interne d'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éthode VerifyFacebookAccessToken (dans 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

Oui, vous pouvez utiliser un jeton d'accès externe pour vous connecter en toute sécurité.

Je vous recommande fortement de suivre ce tutoriel , qui vous montre comment effectuer une authentification basée sur des jetons avec l'API Web 2 à partir de zéro (en utilisant Angular JS comme frontal). En particulier, étape 4 comprend deux méthodes qui vous permettent de vous authentifier à l'aide d'un jeton d'accès externe, par exemple tel que renvoyé par un SDK natif:

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

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

En un mot:

  1. Utilisez le SDK natif pour obtenir un jeton d'accès externe.

  2. Appelez ObtainLocalAccessToken("Facebook", "[fb-access-token]") pour déterminer si l'utilisateur possède déjà un compte (réponse 200), auquel cas un nouveau jeton local sera généré pour vous. Il vérifie également que le jeton d'accès externe est légitime.

  3. Si l'appel à l'étape 2 a échoué (réponse 400), vous devez enregistrer un nouveau compte en appelant RegisterExternal, en passant le jeton externe. Le tutoriel ci-dessus en est un bon exemple (voir AssociateController.js ).

13
Dunc