38

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?

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Obi
  • 3,091
  • 4
  • 34
  • 56
  • 1
    What is not secure in this way? – omeralper Aug 11 '15 at 02:12
  • 2
    @omeralper interestingly it depends. I think I asked this question over 18 months ago, my understanding of oauth and OWIN was a bit limited. To answer your question, it is NOT insecure as long as the transport mechanism is reasonably secured i.e. you make the call to exchange the external access token for a local one (as well as all subsequent calls which have the authorization header) via https. – Obi Aug 11 '15 at 08:42

3 Answers3

29

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!

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
s0nica
  • 1,307
  • 1
  • 15
  • 17
  • 7
    I haven't quite found an answer other than implementing my own Facebook OWIN provider. I did but there are still alot of holes in my knowledge of OWIN and seeing as mine was a commercial rather than academic exercise, I opted to write a very thin quick and dirty non-OWIN implementation. In hindsight, your method might be the best one to take, but there is alot of studying to do to understand OWIN/Katana and all the microsoft blogs just score cheap points describing how to use the templates rather than explaining whats going inside of Katana – Obi Jan 22 '14 at 15:53
  • Hi Obi, I updated my answer with the solution I came up with. – s0nica Jan 23 '14 at 14:12
  • Looks good....actually it looks great and makes a lot of sense. I will try to use it and then mark the answer. Cheers mate – Obi Jan 23 '14 at 14:42
  • Thanks, let me know if something is not working for you or is not clear or either if I made any mistakes :/ – s0nica Jan 24 '14 at 09:03
  • I've updated the answer, I forgot to add the claim that correctly populates the user id – s0nica Feb 10 '14 at 09:43
  • 1
    @s0nica I am trying this approach using Google OAuth and it looks good and I was able to create a token that can be used by my mobile app. My question, since were passing the access_token from the OAuth provider. How do we know that the user is really registered from the mobile app? I notice you have several lines of comment /// Check if the user is already registered - Can you show me how you authenticate the user by just having the access token? – lincx Mar 24 '14 at 04:50
  • Basically, anyone with valid access token from OAuth provider can get token from the web api. – lincx Mar 24 '14 at 04:51
  • 1
    @s0nica i followed your blog http://thewayofcode.wordpress.com/ can you show what the VerifyFacebookAccessToken looks like? and what does "make graph call to the /me endpoint" means? – lincx Mar 24 '14 at 05:33
  • 2
    @csharplinc Hi I created a gist with the method and the view model used, see https://gist.github.com/vgheri/9737178 To make a graph call to the /me endpoint means that you can query the Facebook API called "Graph API" to get information on a certain user, and the user is identified by the access token provided. – s0nica Mar 24 '14 at 09:35
  • @s0nica Thank you very much. But how this graph call see if the request is really coming from the mobile app that requesting it? Sorry for my newbie questions. – lincx Mar 25 '14 at 00:41
  • Hi @lincx , simply it doesn't. Why the graph api should be concerned to check the caller? The facebook_access_token is sufficient and the amount of info returned by the query on the /me endpoint depends on the token itself because it is bound to some permissions. – s0nica Mar 25 '14 at 09:11
  • This solution relies on registering the user using the normal account registration process rather than the external registration process; which seems hacky. – David Poxon Aug 14 '14 at 08:50
  • 1
    @s0nica Thank you for the great answer , I just have one question ? Does it make sense to add a refresh token along with the acess token in the api call ? How can I create a refresh token and appeand the id in the access toke JObject ? see this question for more http://stackoverflow.com/questions/25739710/how-to-create-refresh-token-with-external-login-provider Thanks. – user123456 Sep 10 '14 at 09:15
  • I appreciate your time and effort, but it still could not verify the token – CularBytes May 17 '15 at 14:07
  • how do you handle it when the access token expires?? – raklos Jun 17 '16 at 10:05
  • From where to get **userid** value in `UserLoginInfo loginInfo = new UserLoginInfo("Facebook", model.userid);`? – user1220497 Jan 10 '19 at 10:55
17

Following the great solution from @s0nica, I modified some code in order to integrate with the currently implemented ASP.NET MVC template. s0nica's approach is good, but it isn't fully compatible with the MVC (Non-WebApi) AccountController.

The benefit of my approach is that it works with both ASP.NET MVC and ASP.NET Web API.

The main differences is the claim name. As the claim name FacebookAccessToken is used on this MSDN blog, my approach is compatible with the approach of given in the link. I recommend using it with this.

Note that below code is a modified version of @s0nica's answer. So, (1) walkthrough the above link, and then (2) walkthrough @s0nica's code, and finally (3) consider mine afterwards.

Startup.Auth.cs file.

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

Facebook Login Model for Binding (inner class of 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; }
}

VerifyFacebookAccessToken method (in 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;
}
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Youngjae
  • 24,352
  • 18
  • 113
  • 198
  • In ValidateIdentity, I think perhaps you meant `claims.Count() == 0 || !claims.Any(claim => claim.Type == "FacebookAccessToken")) `, translating to "Reject if there are no claims at all or if there are no claims of the type FacebookAccessToken". Web API by default creates a bunch of other claims in the ticket, like Name and NameIdentifier, so your code didn't work for me. – Lev Dubinets Mar 31 '15 at 01:54
  • I really cant see how this works in an MVC controller - are you using an api controller? – raklos Dec 23 '15 at 14:07
  • @faklos // mainly api controller but checked that mvc controller also works fine. but it could not be working if libraries changed from then. – Youngjae Dec 23 '15 at 15:09
  • @youngjae Thanks, it works great! Only one problem, when login in using the web api, it save the facebook userid on the provider key field in the aspnetuserlogins table and when login in using the website, it saves different user id in the same field. Can you help? – Shaul Zuarets Jun 26 '16 at 17:00
13

Yes, you can use an external access token to securely login.

I highly recommend you follow this tutorial, which shows you how to do token based authentication with Web API 2 from scratch (using Angular JS as the front-end). In particular, step 4 includes two methods that allow you to authenticate using an external access token, e.g. as returned from a native SDK:

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

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

In a nutshell:

  1. Use native SDK to get external access token.

  2. Call ObtainLocalAccessToken("Facebook", "[fb-access-token]") to determine whether the user already has an account (200 response), in which case a new local token will be generated for you. It also verifies that the external access token is legitimate.

  3. If the call in step 2 failed (400 response), you need to register a new account by calling RegisterExternal, passing the external token. The tutorial above has a good example of this (see associateController.js).

Dunc
  • 18,404
  • 6
  • 86
  • 103