ASP.NET Web API and Identity with Facebook login

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!

Leave a Comment