oidc-client
oidc-client copied to clipboard
`renewTokens()` fails silently without throwing an error
Issue and Steps to Reproduce
When we perform an API call, we use the accessToken
and send it to our API endpoint. If the response status is a 401, we refetch the latest accessToken by using the renewTokens()
method. Since this method is a promise, we do wait for it and then reperform the API that initially returned a 401.
Now this method seems to eat the error when there is a peculiar scenario where we have the accessTokenPaylod like the following:
"accessTokenPayload": {
"exp": 1701898951,
"iat": 1701895411,
...
}
Apparently, the exp
and iat
are well before current UNIX timestamp. Therefore what happens is, the method fails (I suppose - not 100% sure). Because of it, we are unable to get the token for re-firing the API.
Versions
7.12.2
Screenshots
Expected
The method should return the valid accessToken
instead of failing silently.
Actual
The method fails silently
Additional Details
- Installed packages:
Hi @abhilashlr7 thank you for your issue. Do you have a sample of your jwt tokens? I think it wiln help me to understand the problem.
Unfortunately I wont be able to share the tokens to the outside world @guillaume-chervet . Is there anything else I can do to help debug this issue?
Adding a few notes that I realised debugging this issue:
- When
renewTokens()
is invoked, therenewTokensAsync()
method is fired which inturn firesrenewTokensAndStartTimerAsync()
method. - Now inside this method, since we don't use
sessionStorage
orServiceWorker
, the line that sets tokens isnavigator.locks.request(...)
. - What I observed is that, the tab that this operation is performed has been idle for quite sometime, and perhaps the token's
iat
andexp
are way behind time. - On another note, I also added an
onEvent
props to theOidcProvider
to observe the events and note down the logs to trace the issue. Doing so, there was this log in console for the eventtryKeepExistingSessionAsync_end
where it said:{ "success": true, "message": "tokens inside storage are valid" }
- And another log for the event
token_timer
where it has-43116
which was basically way behind in time for the tokens. This means, the token has expired and a new token should've been refetched and stored in our Storage (Localstorage). But that did not happen. - Coming back to point 2, since this tab has been alive for quite sometime the lock that it generated initially never gets resolved. Therefore any new locks that were requested gets queued but doesn't get executed. This basically means, the
syncTokens
inside the callback function of the locks request never got executed. - I could also confirm that point 6 is true by printing doing the following in Chrome's console:
This kept returning bothconst state = navigator.locks.query(); console.log(state);
held
andpending
whereheld
was always having 1 item in the array whilepending
had my other 2 items in it. One was for the currentnavigator.locks.request()
while the other was peculiar and is in point 8. - In that same
renewTokensAndStartTimerAsync()
method if you look at, there is a condition to check foroidc.timeoutId
and if so, invokeautoRenewTokens()
. Internally this again triggersrenewTokensAndStartTimerAsync()
which again generates a promise onnavigator.locks.request()
and therefore gets pushed to the pending queue.
Now my thoughts are on, how can we release a lock on the stuck up locks request and if the timer is negative, how do we refetch the tokens and sync into storage.
Would adding { ifAvailable: true }
make it release the promise? I'm not sure, I tried this in local and tried a yarn link
but that asset change never reflects during my debug mode and hence I can't confirm if this is the right fix.
Thank you @abhilashlr7 for all your détail. do you know the lifetime span of your access token? Do you have a sample of your oidc configuration?
@guillaume-chervet here's my config:
scope: 'openid profile email offline_access',
redirect_uri: `/authentication/callback`,
silent_redirect_uri: `/authentication/silent-callback`,
refresh_time_before_tokens_expiration_in_second: 60,
token_request_timeout: 1830,
storage: localStorage,
token_renew_mode: TokenRenewMode.access_token_invalid,
Hi @abhilashlr , thank you for your information. What happen if temporary you set up refresh_time_before_tokens_expiration_in_second: 180 ?
I modified the script locally in debugger with { ifAvailable: true }
and hence the page fetched the right tokens and stored them in the LocalStorage. Now I am unable to go back to that state where it was stuck forever 🤣
The change is here: https://github.com/AxaFrance/oidc-client/pull/1237
I can try with this temporary setup if in case this happens again though.
Also @guillaume-chervet what does refresh_time_before_tokens_expiration_in_second
do, just curious?
Hi @abhilashlr very sorry for the delay, i have been stick this week.
The refresh_time_before_tokens_expiration_in_second will trigger the refresh of the tokens this time before expiration.
I think the if window available make the original problem back. I have to find a solution to remove local on sleeping Window or tab. I will read more the documentation about this.
I this we need to test with an abort controller instead of ifavailable. With a timeout longer than refresh timeout and silentsigin timeout. But it is not simple because of retry integrated. It should be moved to an upper level.
Hi @abhilashlr , I think that the latest error fix this issue.
Hi @guillaume-chervet , Points 3, 4 and 5 in this still seem to be an issue: https://github.com/AxaFrance/oidc-client/issues/1236#issuecomment-1848265003
Hi @abhilashlr7 , i have just seen this today on my new app. It has happened after a computer sleep i have to understand what happened deepe.
Hi @abhilashlr7 , i have just seen this today on my new app. It has happened after a computer sleep i have to understand what happened deepe.
Any updates on this @guillaume-chervet ?
Hi @abhilashlr7 do you still have error? RenewMethod still fail silently (i need to clean this) but now global app behavior should be very stable.
@guillaume-chervet we are facing a new issue now, actually 2. The token gets reset if there are multiple tabs open for some of our users. Another issue is that the renewTokens()
method returns an old token being sent.
I also wanted to know, if lets the internet is not connected for that browser tab, if the token API call fails, is there something like silently failing or not making that api request? For, everytime this token request fails, the authenticatingError
component is being rendered.
I will look at the renew token return an ild tokens. Why do you use this method? In fact the library renew tokens automaticaly.
Yes i have to manage the not connected to internet case. I think i have an idea to test.
I will look at the renew token return an ild tokens. Why do you use this method? In fact the library renew tokens automaticaly.
So, here's why we did it:
- GET /api-1, GET /api-2, GET /api-3 => These are all authn API calls and therefore need the accessToken.
- The access Token is obtained from the localstorage (could also use
useOidcAccessToken()
hook). => Here as well we get the old tokens - Supposing let's say, this access token has expired, when the API call
api-1
is fired, our backend services returns a 401. - Now, we need to make sure we get the latest token and refire the same API. This is exactly where I have hooked up a logic to say, if status code is 401, then FETCH the latest token (
renewTokens()
) assuming it is from the POST api call to the tokens endpoint and then use that response for the headers and refire the failed API.
Now in the last point, if renewTokens()
is going to give us old token then there are high chances our backend is going to reject this requests despite we sending the renewed tokens. That's exactly what I've mentioned in the first comment of this issue.