Skip to main content

WebAPI2 and MVC5 with Google OAuth2 : Access and Refresh token security

So recently I started looking at using WebAPI2, and well, the documentation on what's really going on here, sucks.  My goal here is to allow a user to log-in via OAuth2, pull the access token and the refresh token and handle them safely. This post is really just a place for me to take notes as I dig into this.

Firstly, this is none trivial in Microsoft's implementation.  After digging into this, I must ask if they even thought about how this would be done.  From what I can tell, they are expecting that if you want to get extra information from a provider that you do it in the OnAuthenticated method on the AuthenticationProvider, and then add it to the claim.  And if that's all you need, by all means, do that.

Step one: Requesting the Token

For Google we need the include the access type of offline in our request.  It was talked about on the Katana Project at  GoogleOAuth2Authentication never really get RefreshToken

So given that bit of info we know we can add the extra parameters to the Challenge request.  We do need to be careful here, while Google is fairly liberal with their tokens and letting the user do stuff, but they do have limits to there generosity.

Per the Google OpenID Connect Documentation:

  • Be sure to store the refresh token safely and permanently, because you can only obtain a refresh token the first time that you perform the code exchange flow.
  • There are limits on the number of refresh token that are issued—one limit per client/user combination, and another per user across all clients. If your application requests too many refresh tokens, it may run into these limits, in which case older refresh tokens stop working.

So we need to address both considerations.  My solution?  Well, don't specify the offline requirements in every request.  Only when we link the account do we ask for the refresh token, by setting access_type=offline & prompt=consent Google will ask the user for consent again and then will return you an updated refresh token even if it's not the first time you have asked for one.  NICE.

Step two: Getting the Token

Sweet, Google is returning a refresh token, but how do I get it?

I have seen a few options out there.  All of the ones I found start with customizing the OnAuthenticated method on Authentication Options.  So I too started there

var googleOAuth2AuthenticationOptions = new GoogleOAuth2AuthenticationOptions
{
 ClientId = "YOUR CLIENTID",
 ClientSecret = "YOUR SECRET",
 Provider = new GoogleOAuth2AuthenticationProvider
 {
  OnAuthenticated = OnAuthenticated
 }
};
googleOAuth2AuthenticationOptions.Scope.Add("https://www.googleapis.com/auth/contacts.readonly");
googleOAuth2AuthenticationOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
app.UseGoogleAuthentication(googleOAuth2AuthenticationOptions);
So now what?  Now we need to create the OnAuthenticated method.

Option 1 is to set a property on the Authentication Context
private static Task OnAuthenticated(GoogleOAuth2AuthenticatedContext context)
{
 context.Properties.Dictionary["access_token"] = context.AccessToken;
 context.Properties.Dictionary["refresh_token"] = context.RefreshToken;
 return Task.FromResult<object>(null);
}

Option 2 is to add a claim
private static Task OnAuthenticated(GoogleOAuth2AuthenticatedContext context)
{
 if (!string.IsNullOrEmpty(context.AccessToken))
 {
  context.Identity.AddClaim(new Claim("access_token", context.AccessToken, ClaimValueTypes.String));
 }

 if (!string.IsNullOrEmpty(context.RefreshToken))
 {
  context.Identity.AddClaim(new Claim("refresh_token", context.RefreshToken, ClaimValueTypes.String));
 }

 return Task.FromResult<object>(null);
}

So how do we pick one over the other? To answer that, let's look at the GoogleOAuth2AuthenticationHandler class it's self. (Thank you Microsoft for opening the source up to everyone!)

WARNING: GOING DEEP INTO KATANA SOURCE CODE
The method we are interested in here is AuthenticateCoreAsync(), some interesting bits in here.
  1. The properties dictionary is stored in the "state" attribute that is sent to Google in the authentication request.
  2. At the very end of the method you see a call to "await Options.Provider.Authenticated(context);"  That's where our provider method we declared above is called.
  3. From there an AuthenticationTicket is returned with the Identity (i.e. the claim we built) and the properties dictionary.
But what happens from there?  Pablo M Cibraro's post on Writing an AuthenticationHandler for Katana gives us a good starting point.  But he doesn't go into what happens with the Ticket we return.  For that, it's back to the Katana source code.  NOTE: did you notice the AuthenticateCoreAsync() in the implementing class is protected?  There is a method on the base class called AuthenticateAsync() that handles some initializing before calling into the child class.

One of the places that our Handler's AuthenticateAsync() method is called from is Microsoft.Owin.Security.Infrastructure.OwinRequestExtensions.
public async Task AuthenticateAsync(string[] authenticationTypes, AuthenticateCallback callback, object state)
{
 if (authenticationTypes == null)
 {
  callback(null, null, _handler.BaseOptions.Description.Properties, state);
 }
 else if (authenticationTypes.Contains(_handler.BaseOptions.AuthenticationType, StringComparer.Ordinal))
 {
  AuthenticationTicket ticket = await _handler.AuthenticateAsync();
  if (ticket != null && ticket.Identity != null)
  {
   callback(ticket.Identity, ticket.Properties.Dictionary, _handler.BaseOptions.Description.Properties, state);
  }
 }
 if (Chained != null)
 {
  await Chained(authenticationTypes, callback, state);
 }
}
In here we see both the Identity and the Properties dictionary are passed to a call back delegate.  So what is this call back?  Now we are getting really deep, this Hook class is added to the Owin "security.Authenticate" Environment, we pick this guy back up in Microsoft.Owin.Security.AuthenticationManager where we find our callback in "AuthenticateAsyncCallback" here the identity and the properties are added to a list of "AuthenticateResult" via the state object and returned from the AuthenticateSync() method.

So we just went on a nice little expedition, but what did we learn?
  • We now know how Katana invokes the Authentication Handlers
  • We now know that the AuthenticationTicket we return from our AuthenticationHandler is mapped to a Microsoft.Owin.Security.AuthenticateResult for anyone calling one of the AuthenticateAsync methods on AuthenticationManager.  (this is important, and comes back to us later)
What good does that do us, and how does it help us pick where we want to put this Refresh and Access Token?  One more thing we don't know yet. We don't know how the External Cookie is set.

What we do know is the basics of the authentication flow.
  • User makes a request to our site, we see they are not authenticated and so we return something like this
private class ChallengeResult : HttpUnauthorizedResult
{
 public ChallengeResult(string provider, string redirectUri)
 {
  LoginProvider = provider;
  RedirectUri = redirectUri;
 }
 public string LoginProvider { get; private set; }
 public string RedirectUri { get; private set; }
 public override void ExecuteResult(ControllerContext context)
 {
  var properties = new AuthenticationProperties() { RedirectUri = RedirectUri };
  context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
 }
}
  • That will cause the system to redirect the user, the redirect is triggered by the call to Authentication.Challenge() above, and is ultimately handled in our AuthenticationHandler from ApplyResponseChallengeAsync()
  • We redirect the user to Google in my use case
  • They do their authentication and return back to... well, more correctly the request is handled by Microsoft.Owin.Security.Google.GoogleOAuth2AuthenticationMiddleware.  We registered the middleware when we called "app.UseGoogleAuthentication(googleOAuth2AuthenticationOptions);" in our startup class.
From there GoogleOAuth2AuthenticationHandler.InvokeAsync() is called.

private async Task InvokeReplyPathAsync()
{
 ...
 AuthenticationTicket ticket = await AuthenticateAsync();
 if (ticket == null)
 {
  _logger.WriteWarning("Invalid return state, unable to redirect.");
  Response.StatusCode = 500;
  return true;
 }
 ...
 if (context.SignInAsAuthenticationType != null && context.Identity != null)
 {
  ClaimsIdentity grantIdentity = context.Identity;
  if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
  {
   grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType);
  }
  Context.Authentication.SignIn(context.Properties, grantIdentity);
 }
 ...
}
Oh, and look in there, we are calling AuthenticatAsync(), now we are getting somewhere, and then sure thing, we found the call to Authentication.SignIn().  Inside of AuthenticationManager.Signin() we are storing the properties and identity on a property called "AuthenticationResponseGrant" that through some hoops ends up on the Owin Environment dictionary.

NOTE: see all the calls to context.SignInAsAuthenticationType?  If you dig into app.UseExternalSignInCookie(); you will see a line app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ExternalCookie); well, this is where that comes into play.  If you change this, GoogleOAuth2AuthenticationHandler will write it's session to what ever type is specified.

Ultimately the interesting stuff for AuthenticationResponseGrant ends up in Microsoft.Owin.Security.Cookies.CookieAuthenticationHandler.ApplyResponseGrantAsync() from a Helper class the does "LookupSignIn",  (Like I said, it's some crazy hoops to get here and really not relevant to this post, so I am going to skip that part.)

So now we can see where we are storing what and if it's protected or not.  Here is the part of ApplyResponseGrantAsync() we are most interested in:
protected override async Task ApplyResponseGrantAsync()
{
 AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);
...
 var signInContext = new CookieResponseSignInContext(
  Context,
  Options,
  Options.AuthenticationType,
  signin.Identity,
  signin.Properties,
  cookieOptions);
...
 model = new AuthenticationTicket(signInContext.Identity, signInContext.Properties);
 if (Options.SessionStore != null)
 {
  if (_sessionKey != null)
  {
   await Options.SessionStore.RemoveAsync(_sessionKey);
  }
  _sessionKey = await Options.SessionStore.StoreAsync(model);
  ClaimsIdentity identity = new ClaimsIdentity(
   new[] { new Claim(SessionIdClaim, _sessionKey) },
   Options.AuthenticationType);
  model = new AuthenticationTicket(identity, null);
 }

 string cookieValue = Options.TicketDataFormat.Protect(model);
 Options.CookieManager.AppendResponseCookie(
  Context,
  Options.CookieName,
  cookieValue,
  signInContext.CookieOptions);
...
}
Part one: if (Options.SessionStore != null)
We are going to call into any custom session store that exists and put everything in there, and so if you added a token to your claim or to the properties, then it's sent to your session store and if you are not properly protecting that, it is ripe for the taking.

Part two: string cookieValue = Options.TicketDataFormat.Protect(model);
So two things that can be here, one we didn't have a session store so now we are putting the entire AuthenticationTicket into the cookie, but at least it's encrypted with DPAPI and should be fairly safe.  But what we see is that it doesn't matter were we put the Tokens, it's returned into the cookie.

Second option, if you have a session store, we are only putting the session key down to the client, so no sensitive data is even sent. (NOTE: this is new in version 3.0.0 of Katana)

Conclusion:

If you need to expose the Tokens beyond the scope of the AuthenticationProvider, from the data storage perspective it doesn't make a big difference if you put them in the properties or the claim.  So then you have to ask yourself, what am I going to do with the claim?

Popular posts from this blog

Service to service auth via Azure Active Directory

One of the things I like with the newer services from Azure is the use of Azure AD to authenticate. Azure Keyvault is a perfect example, I can request a JWT from Azure AD and I just pass that to Keyvault in the Authentication header and I am in. Bringing me back to the good old days when we would use Windows AD user as service accounts, you change your password in one place and it's updated everywhere. But this time, you can have more than one password, so your services don't crash as you try to cycle the passwords. (I think I hear angels singing from heaven...) So, how do we make use of this for our own services? So glad you asked. Let's start by pointing you to Microsoft's documentation for  Authentication Scenarios for Azure AD , it's a very good read, and you really should know this stuff before starting this.  The scenario we are looking for is "Daemon or Server Application to Web API", particularly looking at how Web APP 1 ( TodoListDaemon ) and ...

Querying for items in an Array in CosmosDB

If you have spent any time looking at the documentation for Microsoft CosmosDB / DocumentDB, you will see a lot of examples where the data model has a property named "Tags" that is a list of strings.  But you don't see many times they query on something in that Tag property...  One example I saw a query on Tags[0] = "some value" I don't know how often I will need that, but you know, good to know you can do it. After looking through the SQL syntax reference .  The 2 ways I most likely query the Tags would be to use a join on the Tags property or use the ARRAY_CONTAINS function. Side note; the performance of the two methods are basically identical, leading me to believe the query optimizer generates the same instruction sets for both. So unless you have an array of complex objects, just use ARRAY_CONTAINS. Cool, we know how to query for documents that have our tag on them now... One small problem, when you load a million, or even a hundred thousand do...

Service to service auth via Azure Active Directory with ASP.Net Core

Sample configuration for ASP.Net Core 1.1 to use Azure AD for Service to Service Authentication.  Update your Startup.cs to have the following public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(); ... } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseJwtBearerAuthentication(new JwtBearerOptions { Authority = "https://login.microsoftonline.com/{AAD Tenant Name or ID}", Audience = "{Application ID URL}" }); ... } Microsoft.AspNetCore.Authentication.JwtBearer defaults to using OpenID Connect discovery document to validate the bearer token. The Authority is the prefix for the the discovery document.  The middleware will append ".well-known/openid-configuration/" to whatever you pass in to the Authority.  If your IDP has a diffrent endpoint for the discovery document, you can specify the MetadataAddress option, tha...