ASP.NET Identity, OAuth 2 Social Login, Web API 2, and MVC 5 SPAs
Wow, that’s a mouthful. I feel like that lady on the right sometimes.
So, I spent a few days studying SPA applications and how to use external social logins like Facebook and Google with Web API 2.
It’s not for the faint of heart, especially when Microsoft’s Web API template with VS 2013 is broken. Microsoft’s template implementation is about 70% right.
Pile on top, all the inconsistent Web API login routes to think about. See this post for what’s broken. I mostly agree to some extent. In addition, it seems like most “Web API” tutorials out there are outdated. Most of the tutorials have a user’s browser parsing an obscure “auth_token” somewhere along the OAuth login flow. Surprise! In MVC5 + WebAPI2 + Social Login, there is no auth token homie! Now, it’s all handled by the middleware. LoL. Eh, well, whatever, I didn’t want to expose a scary sensitive auth token to the user’s browser anyway. But I digress.
Before we begin, some articles & references that will help you
- IETF’s RFC on OAuth 2 Spec: http://tools.ietf.org/html/rfc6749#section-4.1
- Great Stack Overflow Question on why Auth Tokens & Refresh Tokens
- Using Web API 2 external logins with Facebook & Google.
Distilled down to a single blog post, I present to you, ASP.NET Identity’s OAuth2 Social Login flow with Web API 2:
So it begins, a new user arrives
Figure A:
Figure A, the user arrives and hits your MVC app. Your MVC app sends down your SPA. The user navigates to a login page and Figure B happens.
Figure B:
Your SPA app consumes the JSON from ExternalLogins call and renders some choices. In Figure B, Google and Facebook are configured on the server’s OWIN middleware. Cool.
Figure C:
The user decides to login with “Facebook”. In Figure C, it’s a full-page navigation to the URL of /ExternalLogin?provider=Facebook. Not an AJAX call.
AuthController.cs (originally AccountController.cs from the original template) sees this request, and processing goes up to a ChallengeResult:
// GET api/auth/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if( error != null )
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
//ANONYMOUS will always get trapped here.
if( !User.Identity.IsAuthenticated )
{
return new ChallengeResult(provider, this);
}
A ChallengeResult is returned to the anonymous user. A ChallengeResult looks like:
public class ChallengeResult : IHttpActionResult
{
public ChallengeResult(string loginProvider, ApiController controller)
{
LoginProvider = loginProvider;
Request = controller.Request;
}
public string LoginProvider { get; set; }
public HttpRequestMessage Request { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
Request.GetOwinContext().Authentication.Challenge(LoginProvider);
var response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
RequestMessage = Request
};
return Task.FromResult(response);
}
}
ChallengeResult has an HTTP Status Code of Unauthorized. When MVC is executing the ChallengeResult, MVC notifies the OWIN middleware of an Authentication.Challenge(“Facebook”). The unauthorized status code triggers the OWIN middleware to wake up and process the pending Authorization.Challenge, and transform the unauthorized status into a 302 redirect to an off-site login provider (Facebook in this case) for authorization as shown in Step 7 in Figure C above. Take note, in particular, the 302 redirect to Facebook has “redirect_uri=http://webapp.com/signin-facebook”. This is a callback into the middleware that the user’s browser will hit after they are finished authenticating and authorizing your web app on Facebook. In addition, take 2nd note, the middleware has instructed the browser to set a Correlation.Facebook cookie in Step 7 in Figure C above.
The user’s browser follows the redirect as shown in Figure D below:
Figure D:
In Figure D Step 8 above, the user authenticates with Facebook and authorizes WebApp.
Then, Figure D Step 9, Facebook issues a 302 redirect to the user’s browser back on the OWIN middleware endpoint /signin-facebook. The browser follows the redirect shown in Figure E Step 10 below:
Figure E:
Side Note: When the middleware receives the /signin-facebook callback, the middleware needs to check if the current callback originated from the WebApp to prevent a CSRF attack. The Facebook middleware inspects Correlation.Facebook cookie and matches the cookie with some encrypted state inside the callback that was part of the query string on the way out to Facebook and on the way back in from Facebook. Hewf. Stay with me now…
Next, the OWIN middleware uses the code query string to setup a backchannel with Facebook. The code=AQABC… from Figure E, Step 10, is a code to get a real access_token. The access token is then used to query Facebook’s Open Graph to get some claims about the user, like their Name, picture, email, etc … Step 11 and Step 12 in Figure E.
Finally, the OWIN middleware has enough information to create a ClaimsIdentity of an external login, and does so by granting an AuthenticationTicket associated with an ExternalCookie authentication type. The Facebook middleware packs all the claims from the Facebook backchannel into this cookie called .AspNet.ExternalCookie as shown in Step 13 in Figure E.
Lastly, the OWIN middleware redirects the user’s browser *back* into the MVC layer (remember our old URL that originally kicked off the process? /api/auth/ExternalLogin). Yep. I know. But, this time, we’re going to get past the original ChallengeResult because were coming back as an externally authenticated user with the help of [OverrideAuthentication]+ [HostAuthentication(ExternalCookie)], which sets up the User.Identity.IsAuthenticated to return true skipping the challenge as shown below:
// GET api/auth/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if( error != null )
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
//ANONYMOUS will always get trapped here.
if( !User.Identity.IsAuthenticated )
{
return new ChallengeResult(provider, this);
}
var externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if( externalLogin == null )
{
return InternalServerError();
}
if( externalLogin.LoginProvider != provider )
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
var user = await this.UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey));
var hasRegistered = user != null;
if( hasRegistered )
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var oAuthIdentity = await this.UserManager.GenerateUserIdentityAsync(user, OAuthDefaults.AuthenticationType);
var cookieIdentity = await this.UserManager.GenerateUserIdentityAsync(user, CookieAuthenticationDefaults.AuthenticationType);
var properties = this.UserManager.CreateProperties(user);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
return Redirect(Url.Content("~/app#home"));
}
//so even if we get here, ANONYMOUS will never get this far.
//The browser has been authorized by an external provider.
//The next step is for the browser to call /RegisterExternal
//to register an account. We'll throw up an authorized splash to confirm.
return Redirect(Url.Content("~/app#/authorized"));
}
This time, we hit the bottom of the /api/ExternalLogin call. This is the part where you can customize the login process. The OWIN middleware has already done the hard work for you. In the case where the user has never registered on WebApp, you have a small window of time to use the ExternalCookie login information for purposes of registration. We redirect the user’s browser to ~/app#/authorized, where we throw up a screen and ask for a username. The user clicks “Register” which then calls our Web API endpoint /api/auth/RegisterExternal.
// POST api/auth/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[Route("RegisterExternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
if( !ModelState.IsValid )
{
return BadRequest(ModelState);
}
var info = await Authentication.GetExternalLoginInfoAsync();
if( info == null )
{
return InternalServerError();
}
var user = await this.UserManager.FindAsync(info.Login);
var hasRegistered = user != null;
if( hasRegistered )
{
return BadRequest("External user already registered.");
}
user = new User() { UserName = model.Email, Email = model.Email };
var result = await this.UserManager.CreateAsync(user);
if( !result.Succeeded )
{
return GetErrorResult(result);
}
result = await this.UserManager.AddLoginAsync(user.Id, info.Login);
if( !result.Succeeded )
{
return GetErrorResult(result);
}
return Ok();
}
And our user now has a local account.
Next blog post I’ll will show how to transform the ExternalCookie AuthenticationType into a local usable access_token for Token Barer Authentication for Web API during the life time of the SPA application.
HTH,
Brian Chavez
Comments
Attiqe Ur Rehman
Bryan,
It’s a nice and detail article and everything is explained the way it should be.
Thumbs up.
Thanks,
Attiqe
Tom
Thanks Brian!
This is very well explained, there are numerous posts out there that attempt to dissect this, but none of them do it so clearly. Kind of a shame this doesn’t show up better in google searches.
Have you gotten around to writing the second part to this you mention at the end of the post?
That appears to be triggering the middleware to call into the built in oauth endpoint to get an access token, correct? Would be great to hear your take on that part.
Brian Chavez
Hi Tom. No, I haven’t got around to it but I but will soon. :)
Michal Belas
Hi, thanks for great tutorial.
But i have question about your next blog post where you show us "ow to transform the ExternalCookie AuthenticationType into a local usable access_token for Token Barer Authentication for Web API during the life time of the SPA application.", because i have very big problem with this. I have web api, and i want immediatelly login user after register from external providers. But i dont know how return access_token and refresh token.
Thank you very much.
Have a nice day.
Michal Belas
Hi Brian,
Thanks for great post, its very helpfull for me.
I want to ask you about your second part you mention at the end of the post?
I exactly need it at my project.
Thanks, and great job again ;)
Pietro Perugino
Thank you for this article. Very well written. And cleared a lot up
Bob Yuan
awesome post, I’m really looking forward to your second part.
Diego Falciola
This is a very detailed article. A good resource that could be of help for people looking into this is https://github.com/tjoudeh/AngularJSAuthentication.
Also, is there another blog post on this matter? Or the sourcecode available of this implementation?
Thanks!
Mark F
Great post! This helped me understand what a lot of other posts glazed over.
Thank you!
Leave a comment
Your email address will not be published. Required fields are marked *