Reputation: 6650
I have code that works - MVC app using Google Calendar API and Gmail API with OAuth2 Authentication from Google. The code works. When the page is loaded the data from both services is displayed. And I have a Javascript timer to refresh the data with certain interval (20 min). So everything works as expected until at some point of time (after some time interval I guess) it starts throwing an exception: Error:"invalid_grant", Description:"", Uri:"". The exception has no InnerException and has only that error message and this info in StackTrace (here on the screenshot):
I would really appreciate if someone has an idea what could be the reason for that error. And what is that "c:\code\google.com...." line in stack trace message, I have no "c:\code" folder on my disk. I have found a few posts related to the same error, but unfortunately they didn't help to understand the problem. Maybe with more details like this screenshot someone has more info on the subject. Thanks a lot.
What I found out - is that AppPool recycling temporary solves the problem. But then, after some time, it comes back again. What doest it have to do with AppPool recycling?
Upvotes: 1
Views: 3272
Reputation: 10410
It's also possible that the server clock is out of sync. For some reason mine was not able to sync against an internet clock and was running 6 minutes fast. Resetting it to the correct time worked.
Upvotes: 1
Reputation: 6650
Well, after more reading I found the reason of this exception.
https://developers.google.com/accounts/docs/OAuth2#expiration
https://developers.google.com/analytics/devguides/config/mgmt/v3/mgmtAuthorization?#helpme
Invalid Grant: The refresh token limit has been exceeded (default is 25). That's all.
According to these documentation: There is currently a 25-token limit per Google user account. If a user account has 25 valid tokens, the next authentication request succeeds, but quietly invalidates the oldest outstanding token without any user-visible warning.
If the application attempts to use an invalidated refresh token, an invalid_grant error response is returned. The limit for each unique pair of OAuth 2.0 client and Google Analytics account is 25 refresh tokens (note that this limit is subject to change).
Understood, they limit # of refresh tokens to 25, but they don't say what to do when you need to go above that limit. Arghhh... I have been experimenting and found a solution how to bypass that limitation. It seems indeed that recycling the Application Pool solves the problem (of course untill next 25-limit is reached). We can manually recycle the AppPool from IIS or by running the command:
c:\Windows\System32\inetsrv\appcmd.exe recycle apppool /apppool.name:AppPoolName
You can schedule that command to execute every night or every hour, whatever...
But I found a have a programmatic solution:
Override OnException method for your controller (it's for MVC app)
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled) return;
// Log exception details
Global.LogException(filterContext.Exception, EventLogEntryType.Error);
if (filterContext.Exception.Message.Contains("invalid_grant"))
{
// Invalid Grant: The refresh token limit has been exceeded (default is 25).
// https://developers.google.com/accounts/docs/OAuth2#expiration
// https://developers.google.com/analytics/devguides/config/mgmt/v3/mgmtAuthorization?#helpme
Global.RecycleAppPool();
Global.LogException(new Exception("AppPool has been recycled"), EventLogEntryType.Information);
Response.Redirect("Index");
}
var actionName = filterContext.RouteData.Values["action"].ToString();
// Return friendly error message
var errorMessage = string.Format("Action {0} failed with error: {1}. Please try again.", actionName, filterContext.Exception.Message);
filterContext.Result = Content(errorMessage);
filterContext.ExceptionHandled = true;
base.OnException(filterContext);
}
Where RecycleAppPool is defined like this (this operation is fast, not like restarting IIS :):
public static void RecycleAppPool()
{
ServerManager serverManager = new ServerManager();
ApplicationPool appPool = serverManager.ApplicationPools["Homepage"];
if (appPool != null)
{
if (appPool.State == ObjectState.Stopped) appPool.Start();
else appPool.Recycle();
}
}
So, in case of invalid_grant exception, the exception "swallowed": logged, apppool is recycled and the limit for refresh tokens is reset. Hope this helps.
Please let me know if you find some issues.
Upvotes: 1