xero-node: Getting invalid_grant after about a week
Version 4.4.3
We deployed our pre-tested Oauth2 app to prod (pre-tested for a couple of days), and about a week later, we started getting invalid_grants when trying to refresh the token. Not sure where and why it goes wrong. For the sake of simplicity, before every xero request, an ensureAccessTokenValidity function is ran and a CRON job 2 times a week to re-new the refresh token. I’m aware refresh tokens are valid for 30 days, but to be secure and account for any CRON job outages, we try to renew it 8-9 times a month.
This is the function which is called before every Xero request:
async ensureAccessTokenValidity(region: InstanceType<RegionSchema>) {
const checkAndTryToRefresh = async () => {
const xa = region.xeroAccount;
try {
this.Log.debug("ACCESS TOKEN SET:", JSON.stringify(xa.getXeroAccessTokenSet(), null, 4));
const tokenSet = xa.getXeroAccessTokenSet();
if (!tokenSet) throw new Error("Xero is not connected.");
if (xa.getXeroAccessTokenSet().expired()) {
this.Log.info("tokenset expired");
}
await xero.setTokenSet(xa.getXeroAccessTokenSet())
await xero.accountingApi.getAccounts(region.xeroAccount.xeroTenantId)
return [true, xa.getXeroAccessTokenSet()]
}
catch (e) {
if (e.response && e.response.statusCode && e.response.body) {
const {body} = e.response;
const tokenExpiredError = body.Status == 401 && body.Detail.includes("TokenExpired");
const authUnsuccessfulError = body.Status == 403 && body.Detail.includes("AuthenticationUnsuccessful")
if (tokenExpiredError || authUnsuccessfulError) {
const tokenSet = await xero.refreshToken();
xa.setXeroAccessTokenSet(tokenSet)
await RegionModel.updateOne({ _id: region._id }, {
'xeroAccount.xeroAccessToken': xa.xeroAccessToken
})
await xero.setTokenSet(tokenSet);
return [true, tokenSet]
}
}
return [false, e]
}
}
const maxTries = 3;
let tries = 0;
do {
const result = await checkAndTryToRefresh();
if (result[0] == true) {
return result[0];
} else
if (tries == maxTries - 1) {
throw result[1];
} else {
await new Promise(resolve => setTimeout(resolve, 2000));
}
tries++;
} while (1)
}
I just added the retry logic, so the invalid_grants happened without it. Now, since the invalid_grant is quite indescriptive, I’m not sure what’s going on. This same function is ran by the CRON too, so basically I try to xero.accountingApi.getAccounts(region.xeroAccount.xeroTenantId) and if it fails, refresh the token.
Can it be because of Xero & our server’s time mismatch? Haven’t checked for that, but I read it happens with Google OAuth2. What possible reasons can there be?
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 20 (5 by maintainers)
We’ve also experienced the same issue (and have a similar brute force method to refresh the token - although it’s initiated by the user when they view this particular problem). It would be good to resolve though (even though it’s difficult to reproduce), as we’ve been experiencing this issue in prod for the last 6 weeks or so (it’s not a great look for the users of the system).
I think the problem that I had was that a long-running process would determine part way through that the access token was to be refreshed. A previously cached refresh token was then used - cached from the start of the long running process - to get another refresh token. At the same time, another long running process was active and it tried to do the same thing. I believe if Xero sees an expired refresh token used to request a new access token then it will disconnect the application and return an invalid_grant. At this point the only way to resolve the issue is to have the user reauthorise the connection via your OAuth connection process.
Hey guys - yeah @communig8-public got it right. I will work to make the documentation on that more concise & clear.
https://github.com/XeroAPI/xero-node/issues/382#issuecomment-808827084
I have exact same issue in my app and lot of people’s has the same issue
Just a thought, I was suffering the invalid_grant issue until I realised that by using the refresh_token in the original tokenSet to perform a refresh causes that refresh_token to become expired and any subsequent refresh has to use the new refresh_token supplied to the new tokenSet. So the new tokenSet has to be persisted in some way for later use.
Yes, they were valid. Started happening only after a few days, then clients now almost daily have to reconnect. Created a standalone app and set it up on its own server (planned to share its code with you when we start receiving invalid_grants but that hasn’t happened yet). It uses the same code, stripped down to just periodically calling getAccounts() and trying to renew the token, and that’s been working fine for almost 2 weeks now. There are a couple more areas which I need to check tho