MVC 5 Web API with Facebook access token to RegisterExternal without need of Cookie

后端 未结 2 1561
温柔的废话
温柔的废话 2020-12-07 23:46

Setup: New MVC5 Project with just Web API. Added Facebook AppId and Secret.
I can get Token for my Web API from Token endpoint by passing

相关标签:
2条回答
  • 2020-12-08 00:01

    Before everything, this is NOT A FULL Answer, this is just a note or an addition for the answer to avoid some problems which could cost you handful of days (in my case 3 days)

    The previous answer is the full answer it just lacks from one thing, which is the following:
    if you specified a role for the Authorize attribute, for example [Authorize("UserRole")] , the previous setup will still give you 401 error because the solution does not set the RoleClaim

    and to solve this problem you have to add this line of code to the RegisterExternalToken method

    oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, "UserRole"));
    
    0 讨论(0)
  • 2020-12-08 00:16

    I was mistaken that it accepts the Social Token with cookie! It doesn't accept any External Token directly.

    The thing is.. MVC 5 is taking care of everything for us, i.e. collecting token from Social Medias and validating/processing it. After that it generates a local token.

    The RegisterExternal method also requires cookies to be maintained, the solution does not.

    I have written a blog post which will explain in detail. Added the straight forward answer below. I aimed to make it blend and feel integral part of Login/Signup flow of default MVC Web API to make sure its easy to understand.

    After the below solution, Authorize attribute must be as below to work or you will get Unauthorized response.

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

    Use ExternalBearer if you want to allow only Tokens to use API, use ApplicationCookie if you want to allow only Logged cookie to use API i.e. from a website. User both if you want to allow the API for both.

    Add this action to 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("ffffd, dd MMM yyyy HH':'mm':'ss 'GMT'")),
            new JProperty(".expires", currentUtc.Add(TimeSpan.FromDays(365)).ToString("ffffd, dd MMM yyyy HH:mm:ss 'GMT'"))
        );
        return Ok(token);
    }
    

    Add this helper method to ExternalLoginData class in helper region in 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")
        {
            return null; // not implemented yet
            //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)
        };
    }
    

    and finally, the RegisterExternalTokenBindingModel being used by the action.

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

    Yes, we pass the email along with Token details while registering, this will not cause you to change the code when using Twitter, as Twitter doesn't provide users email. We verify token comes from our app. Once email registered, hacked or somebody else's token cannot be used to change email or get a local token for that email as it will always return the local token for the actual user of the Social Token passed regardless of the email sent.

    RegisterExternalToken endpoint works to get token in both ways i.e. register the user and send the Local token or if the user already registered then send the token.

    0 讨论(0)
提交回复
热议问题