Rob
Rob

Reputation: 6871

How to authenticate facebook web api in asp.net core 2

I'm currently building my very first mobile app in Xamarin.Forms. The app has a facebook login and after the user has been logged in I'm storing the facebook token because I want to use it as a bearer-token to authenticate any further requests against an API.

The API is a .NET core 2.0 project and I am struggling to get the authentication working.

In my Xamarin.Forms app the facebook token is set as bearer-token with the following code;

_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", UserToken);

As far as I know, this properly sets the bearer token in the headers of the request. I've talked to a colleague of mine about this and he told me to take a look at Identityserver4 which should support this. But for now, I've decided not to do that since for me, at this moment, it is overhead to implement this. So I've decided to stay with the idea to use the facebook token as bearer token and validate this.

So the next step for me is to find a way to authenticate the incoming bearer token with Facebook to check if it is (still) valid. So I've configured the startup for my API projects as following;

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(o =>
        {
            o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddFacebook(o =>
        {
            o.AppId = "MyAppId";
            o.AppSecret = "MyAppSecret";
        });

        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        //Enable authentication
        app.UseAuthentication();

        //Enable support for default files (eg. default.htm, default.html, index.htm, index.html)
        app.UseDefaultFiles();

        //Configure support for static files
        app.UseStaticFiles();

        app.UseMvc();
    }
}

But when I'm using postman to do a request and test if everything is working I'm receiving the following error;

InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

What am I doing wrong here?

EDIT: In the mean time if been busy trying to find a solution for this. After reading a lot on Google it seems thatadding an AuthorizationHandler is the way to go at the moment. From there on I can make request to facebook to check if the token is valid. I've added the following code to my ConfigureServices method;

public void ConfigureServices(IServiceCollection services)
    {
        //Other code

        services.AddAuthorization(options =>
        {
            options.AddPolicy("FacebookAuthentication", policy => policy.Requirements.Add(new FacebookRequirement()));
        });

        services.AddMvc();
    }

And I've created a FacebookRequirement which will help me handling the policy;

public class FacebookRequirement : AuthorizationHandler<FacebookRequirement>, IAuthorizationRequirement
    {
        private readonly IHttpContextAccessor contextAccessor;
        public FacebookRequirement(IHttpContextAccessor contextAccessor)
        {
            this.contextAccessor = contextAccessor;
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FacebookRequirement requirement)
        {
            //var socialConfig = new SocialConfig{Facebook = new SocialApp{AppId = "MyAppId", AppSecret = "MyAppSecret" } };
            //var socialservice = new SocialAuthService(socialConfig);

            //var result = await socialservice.VerifyFacebookTokenAsync()
            var httpContext = contextAccessor.HttpContext;

            if (httpContext != null && httpContext.Request.Headers.ContainsKey("Authorization"))
            {
                var token = httpContext.Request.Headers.Where(x => x.Key == "Authorization").ToList();
            }

            context.Succeed(requirement);

            return Task.FromResult(0);
        }
    }

The problem I'm running into now is that I don't know where to get the IHttpContextAccessor. Is this being injected somehow? Am I even on the right path to solve this issue?

Upvotes: 4

Views: 6905

Answers (1)

Rob
Rob

Reputation: 6871

I ended up creating my own AuthorizationHandler to validate incoming requests against facebook using bearer tokens. In the future I'll probably start using Identityserver to handle multiple login types. But for now facebook is sufficient.

Below is the solution for future references.

First create an FacebookRequirement class inheriting from AuthorizationHandler;

public class FacebookRequirement : AuthorizationHandler<FacebookRequirement>, IAuthorizationRequirement
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FacebookRequirement requirement)
        {
            var socialConfig = new SocialConfig { Facebook = new SocialApp { AppId = "<FacebookAppId>", AppSecret = "<FacebookAppSecret>" } };
            var socialservice = new SocialAuthService(socialConfig);

            var authorizationFilterContext = context.Resource as AuthorizationFilterContext;
            if (authorizationFilterContext == null)
            {
                context.Fail();
                return Task.FromResult(0);
            }

            var httpContext = authorizationFilterContext.HttpContext;
            if (httpContext != null && httpContext.Request.Headers.ContainsKey("Authorization"))
            {
                var authorizationHeaders = httpContext.Request.Headers.Where(x => x.Key == "Authorization").ToList();
                var token = authorizationHeaders.FirstOrDefault(header => header.Key == "Authorization").Value.ToString().Split(' ')[1];

                var user = socialservice.VerifyTokenAsync(new ExternalToken { Provider = "Facebook", Token = token }).Result;
                if (!user.IsVerified)
                {
                    context.Fail();
                    return Task.FromResult(0);
                }

                context.Succeed(requirement);
                return Task.FromResult(0);
            }

            context.Fail();
            return Task.FromResult(0);
        }
    }

Add the following classes which will contain the configuration an represent the user;

public class SocialConfig
    {
        public SocialApp Facebook { get; set; }
    }

    public class SocialApp
    {
        public string AppId { get; set; }
        public string AppSecret { get; set; }
    }

    public class User
    {
        public Guid Id { get; set; }
        public string SocialUserId { get; set; }
        public string Email { get; set; }
        public bool IsVerified { get; set; }
        public string Name { get; set; }

        public User()
        {
            IsVerified = false;
        }
    }

    public class ExternalToken
    {
        public string Provider { get; set; }
        public string Token { get; set; }
    }

And last but not least, the SocialAuthService class which will handle the requests with facebook;

public class SocialAuthService
    {
        private SocialConfig SocialConfig { get; set; }

        public SocialAuthService(SocialConfig socialConfig)
        {
            SocialConfig = socialConfig;
        }

        public async Task<User> VerifyTokenAsync(ExternalToken exteralToken)
        {
            switch (exteralToken.Provider)
            {
                case "Facebook":
                    return await VerifyFacebookTokenAsync(exteralToken.Token);
                default:
                    return null;
            }
        }

        private async Task<User> VerifyFacebookTokenAsync(string token)
        {
            var user = new User();
            var client = new HttpClient();

            var verifyTokenEndPoint = string.Format("https://graph.facebook.com/me?access_token={0}&fields=email,name", token);
            var verifyAppEndpoint = string.Format("https://graph.facebook.com/app?access_token={0}", token);

            var uri = new Uri(verifyTokenEndPoint);
            var response = await client.GetAsync(uri);

            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                dynamic userObj = (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);

                if (appObj["id"] == SocialConfig.Facebook.AppId)
                {
                    //token is from our App
                    user.SocialUserId = userObj["id"];
                    user.Email = userObj["email"];
                    user.Name = userObj["name"];
                    user.IsVerified = true;
                }

                return user;
            }
            return user;
        }
    }

This will validate the Facebook token coming from the request as bearer token, with Facebook to check if it's still valid.

Upvotes: 5

Related Questions