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?
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!
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;
}
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:
Use native SDK to get external access token.
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.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).