Search code examples
facebookasp.net-web-apiasp.net-identityfacebook-android-sdkfacebook-ios-sdk

ASP.NET Web API and Identity with Facebook login


In the Facebook authentication flow for ASP.NET Identity, the Facebook OAuth dialog appends a code rather than an access token to the redirect_url so that the server can exchange this code for an access token via e.g.:

http://localhost:49164/signin-facebook?code=…&state=…

My problem is that my client is a mobile app which uses the Facebook SDK, and that straight away gives me an access token. Facebook says using the SDK always gives you an access token, so can I just give this directly to ASP.NET Web API?

I understand this is not very secure, but is it even possible?


Solution

  • I don't know if you ever found a solution, but I'm trying to do something similar and I'm still putting the pieces of the puzzle together. I had tried to post this as a comment instead of an answer, as I do not provide a real solution, but it's too long.

    Apparently all of the WebAPI Owin OAuth options are browser based—that is, they require lots of browser redirect requests that do not fit a native mobile app (as required for my case). I'm still investigating and experimenting, but as briefly described by Hongye Sun in a comment to his blog post, to login with Facebook the access token received using the Facebook SDK can be verified directly via the API by making a graph call to the /me endpoint.

    By using the information returned by the graph call, you can then check if the user is already registered or not. At the end, we need to sign-in the user, maybe using Owin's Authentication.SignIn method, returning a bearer token that will be used for all subsequent API calls.

    EDIT: Actually, I got it wrong. The bearer token is issued on calling /Token endpoint, which on input accepts something like:

    grant_type=password&username=Alice&password=password123
    

    The problem here is that we do not have a password—that's the whole point of the OAuth mechanism—so how else can we invoke the /Token endpoint?

    UPDATE: I finally found a working solution and the following is what I had to add to the existing classes to make it work:

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

    And in AccountController, I added the following action:

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

    That's it! The only difference I found with the classic /Token endpoint response is that the bearer token is slightly shorter and the expiration and issue dates are in UTC instead that in GMT (at least on my machine).

    I hope this helps!