Heads up!
The blog has moved!
If you are interested in reading new posts, the new URL to bookmark is http://blog.valeriogheri.com/
The context
I’m working on a project where I have a RESTful HTTP API implemented using the WebAPI framework that is consumed by a native Android application.
This application allows the user to login using their Facebook account and this feature is implemented using the Facebook SDK for Android.
Now the Facebook SDK always gives you an access token if the OAuth process completes correctly and that’s exactly where I found some hurdles integrating with the WebAPI Asp.Net Identity flow: in the facebook authentication flow for asp.net identity, the facebook oath dialog appends a code rather than access token to the redirect_url, so that the server can exchange this code for an access token via http://localhost:49164/signin-facebook?code=…&state=…
Obviously this is not good for our scenario as we already have the facebook access token!
The goal is not to reinvent the wheel, but to find a solution that integrates with the existing OWIN authentication and authorization pipeline.
What we want is to find a way to use existing Asp.Net Identity methods to register the claim identity, so that the system knows about the user, and to generate an API Bearer token that will be given back to the client and that will need to be supplied for each subsequent call to the API endpoints.
The solution
The proposed solution acts on two different files:
Startup.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = () => | |
| { | |
| 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 | |
| }; | |
| // This is a key step of the solution as we need to supply a meaningful and fully working | |
| // implementation of the OAuthBearerOptions object when we configure the OAuth Bearer authentication mechanism. | |
| // The trick here is to reuse the previously defined OAuthOptions object that already | |
| // implements almost everything we need | |
| OAuthBearerOptions = new OAuthBearerAuthenticationOptions(); | |
| OAuthBearerOptions.AccessTokenFormat = OAuthOptions.AccessTokenFormat; | |
| OAuthBearerOptions.AccessTokenProvider = OAuthOptions.AccessTokenProvider; | |
| OAuthBearerOptions.AuthenticationMode = OAuthOptions.AuthenticationMode; | |
| OAuthBearerOptions.AuthenticationType = OAuthOptions.AuthenticationType; | |
| OAuthBearerOptions.Description = OAuthOptions.Description; | |
| // The provider is the only object we need to redefine. See below for the implementation | |
| 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] | |
| //Here we use the OAuthBearerOptions object | |
| OAuthBearerAuthenticationExtensions.UseOAuthBearerAuthentication(app, OAuthBearerOptions); | |
| //[More boilerplate code] | |
| } | |
| } | |
| 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.Count() == 0 || claims.Any(claim => claim.Issuer != "Facebook" && claim.Issuer != "LOCAL_AUTHORITY" )) | |
| context.Rejected(); | |
| return Task.FromResult<object>(null); | |
| } | |
| } |
AccountController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [HttpPost] | |
| [AllowAnonymous] | |
| [Route("FacebookLogin")] | |
| public async Task<IHttpActionResult> FacebookLogin([FromBody] string token) | |
| { | |
| if (string.IsNullOrEmpty(token)) | |
| { | |
| return BadRequest("Invalid OAuth access token"); | |
| } | |
| var tokenExpirationTimeSpan = TimeSpan.FromDays(14); | |
| ApplicationUser user = null; | |
| // Get the fb access token and make a graph call to the /me endpoint | |
| var fbUser = await VerifyFacebookAccessToken(token); | |
| if (fbUser == null) | |
| { | |
| return BadRequest("Invalid OAuth access token"); | |
| } | |
| // Check if the user is already registered | |
| user = await UserManager.FindByNameAsync(fbUser.Username); | |
| // If not, register it | |
| if (user == null) | |
| { | |
| var randomPassword = System.Web.Security.Membership.GeneratePassword(10, 5); | |
| user = await RegisterUserAsync(fbUser.Username, randomPassword, fbUser.ID); | |
| var customer = await RegisterCustomerAsync(fbUser.FirstName, fbUser.LastName, fbUser.Email, user); | |
| } | |
| // Sign-in the user using the OWIN flow | |
| var identity = new ClaimsIdentity(Startup.OAuthBearerOptions.AuthenticationType); | |
| identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName, null, "Facebook")); | |
| // This is very important as it will be used to populate the current user id | |
| // that is retrieved with the User.Identity.GetUserId() method inside an API Controller | |
| 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); | |
| 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()) | |
| ); | |
| // Return OK | |
| return Ok(blob); | |
| } |
I posted this solution on StackOverflow to answer the following question: http://stackoverflow.com/questions/21092723/webapi-asp-net-identity-facebook-login
I hope this helps,
Valerio