okta-react
okta-react copied to clipboard
Token renewal fails silently for some users, some of the time (3.0.4/3.0.8, third-party cookies allowed)
Our setup:
- SPA, authorization code flow with PKCE.
- Okta and app domains are different.
- We know for sure that users who complain about this issue have third-party cookies allowed. It does not fail for the same users all the time. Sometimes it works for them (I see
Token ... renewed
logs from them). - We do override
isAuthenticated
as recommended:
isAuthenticated: async () => {
const idToken = await authService.getIdToken();
const accessToken = await authService.getAccessToken();
return !!(idToken && accessToken);
},
autoRenew: true,
expireEarlySeconds: 30 * 60
-
idToken
andaccessToken
expiration time is set to 60 minutes in Okta. - As a workaround attempt, we call
authService.getIdToken()
before each request to our backend (we then sendidToken
to the backend). This does not seem to help. - We collect frontend logs via a separate unauthenticated endpoint. I subscribed to tokenManager's
expired
,renewed
, anderror
events. Here's a sequence of logs I'm seeing in one of the recent cases:
- Going to make a backend request (seen on backend side)
-
Token with key accessToken has expired
-
Token with key idToken has expired
- Going to make a backend request (not seen on backend side) ...
- Going to make a backend request (not seen on backend side)
There's no error
event or renewed
event around. Note that "going to make a backend request" is logged before we call authService.getIdToken
, so the most likely explanation for why it does not reach our backend is that authService.getIdToken
just hangs. Could it be related to the fact that Google Chrome does not start a new request if an identical request is already running? Is there some timeout for the token renewal request?
Upgraded to 3.0.8 a few days ago, the issue persists.
Could it be related to @okta/okta-signin-widget starting the renewal process at the same time?
I am also seeing this issue, we have set our okta access_token expiration to 5 minutes and it is very apparent that it doesn't always make a call to /authorize
when the token expires
@patkovskyi I do not think it is related to the signin-widget for 2 reasons:
- widget does not read from token manager, so it should not trigger auto renew process
- widget should only be created/rendered on login page, so it should not be relevant when the user has an active session
In your issue you mentioned Google Chrome. Does this issue only occur on chrome, or do you see it in other browsers? If it is only in chrome the hunch about multiple requests may be the culprit (to my knowledge only chrome will cancel requests), this would indicate a potential concurrency issue. Do you have multiple instances of auth-js or okta-react on the page?
@Dewar0019 The current version of okta-react
uses a "passive" auto renew, so it will only attempt to renew tokens when they are read from the token manager. More information here: https://github.com/okta/okta-oidc-js/issues/744#issuecomment-668247988
@aarongranick-okta I'm a bit confused, in the https://github.com/okta/okta-oidc-js/issues/744#issuecomment-668247988 you've mentioned that the auto-renew mechanism is "active" since 3.0.3 version, no?
Anyway, I tried both approaches:
- relying on "active" auto-renewal and only getting
idToken
fromauthState
- calling
authService.getIdToken()
every time I need to use the token
and neither worked reliably.
The first approach leads to some users eventually sending expired JWT tokens to the backend, the second approach causes some calls to authService.getIdToken()
hang indefinitely. The strangest thing is that the same users who experience this problem sometimes have a successful token renewal, so it does not seem strictly related to the user environment (browser, cookie settings, etc.).
We are also facing this issue and it is maddening. It only happens intermittently, but when it does the user is in a weird state and all network requests fail because their token is indeed invalid BUT the Okta SDK does not auto renew their token nor does it redirect users to login again.
Sometimes having the user logout manually fixes the issue. Sometimes we have to have the user clear local storage and cookies in order for them to be able to use our application again.
This has been happening for a while by the way as https://github.com/okta/okta-oidc-js/issues/744 indicates. It would be nice if we could get an update from somebody from the Okta team on this issue as I do see there is an internal refs number listed in the issue I linked.
We are using @okta/okta-react
3.0.8
btw.
@nryoung - do you have any information on what triggers this condition?
@swiftone Sure, it actually just happened to me again, so here is what I am seeing:
In our dev environment we have the token timeout set to 5 mins to help diagnose this issue. Well, I see that Okta attempted to POST
to the following URL (with some redactions of course):
https://<redacted>.okta.com/oauth2/<redacted>/v1/token
However, it returned a 400
with the following response:
{"error":"invalid_grant","error_description":"PKCE verification failed."}
I am now in an invalid state now. So, any requests I make to our backend fail with a 400
and the response:
{"errors":[{"message":"Context creation failed: Unable to verify token with Okta","extensions":{"code":"UNAUTHENTICATED"}}]}
The only solution for me now is to attempt to logout, which sometimes fails because of this issue: https://github.com/okta/okta-oidc-js/issues/861 so if that fails then I have to manually clear my local storage.
I should be clear, the above error response we get back is not the only error we get back from Okta that causes this state. Below you can see other error messages that cause us to get in to this state as well:
OAuthError: User is not assigned to the client application.
(Even though they are assigned this application)
AuthSdkError: OAuth flow response state doesn't match request state
(it claims the JWT is issued in the future even though decoding the token it is not)
For whatever reason the Okta SDK has an invalid token at this point and redirects to the root /
of our application even though that is wrapped in a SecureRoute
and it is not redirecting us to Okta to login again.
@nryoung Do you have multiple Okta apps running on the same domain? Is it possible other apps are reading/writing to the same token storage? Also are you using the onSessionExpired
option?
@aarongranick-okta
Do you have multiple Okta apps running on the same domain?
We do not, only one application per domain.
Is it possible other apps are reading/writing to the same token storage?
It's not we only have one React app running per domain.
Also are you using the
onSessionExpired
option?
We are not.
I'm also having this issue, I moved from authState.accessToken
to authService.getAccessToken()
but still get the same behavior.
For those of you facing this issue we have implemented a workaround that seems to be working until this is fixed internally within the okta sdk:
const checkExistenceOfOktaAccessToken = () => {
try {
JSON.parse(localStorage.getItem('okta-token-storage')).accessToken;
} catch (error) {
window.location.reload();
}
};
We manually check for the existence of the okta token on every render. When find that it no longer exists, we force the whole app to reload as it should exist at this point in the application no matter what.
It's not the greatest workaround but until we can can actually rely on the getAccessToken
method and the token renewal to work correctly that is the best we have come up with.
A very helpful Okta support engineer responded to an inquiry about this and suggested what has turned out to be a very effective work-around. See safelyGetToken
below which is used everywhere in place of authService.getAccessToken
:
import { Switch, Route, Redirect } from "react-router-dom";
import config from "./api/config";
import NotFoundPage from "./components/NotFoundPage";
import routes from "./routes";
import React from "react";
import { OurRestProviderProvider } from "./rest";
import { LoginCallback, SecureRoute, Security, useOktaAuth } from "@okta/okta-react";
function OurSecureRoute(props: any) {
const { authService } = useOktaAuth();
const safelyGetToken = async () => {
const token = await authService.getAccessToken();
if (token) return token;
// For some reason, access token in local storage is undefined.
// May happen if no network connectivity at the time the token would be renewed.
// Solution: refetch token from source and update token manager
const response = await authService._oktaAuth?.token?.getWithoutPrompt();
const accessToken = response?.tokens?.accessToken;
if (accessToken) {
authService._oktaAuth?.tokenManager?.add("accessToken", accessToken);
return accessToken.accessToken;
}
};
return (
<OurRestProvider
base={config.SERVICE_API_URL}
requestOptions={async () => ({ headers: { Authorization: `Bearer ${await safelyGetToken()}` } })}
>
<SecureRoute {...props} />
</RestfulProvider>
);
}
function AppWithSecurity() {
return (
<Switch>
<Route path="/implicit/callback" component={LoginCallback} />
<Redirect exact from="/" to={routes.DASHBOARD} />
<OurSecureRoute path="*" component={NotFoundPage} />
</Switch>
);
}
@amacleay-cohere - is this change still relevant?
Thanks for the reminder! We've had this workaround in place but it hasn't been triggered since we updated to v4, so we can remove it now. Thanks @amcdnl !
Hmmm - I've been having similar issues - I'm on v4 - I applied this tweak today - will let everyone know if it solves me problem.
@amcdnl has applying the tweak solved your problem?
@amcdnl @EricHedden With current version of @okta/okta-react
and @okta/okta-auth-js
it should not be necessary to apply any workarounds or set an isAuthenticated
function. Out of the box, standard token renews should work without issue (if 3rd party cookies are not blocked). refresh token-based renews (offline_access scope) should work regardless of cookies.
@aarongranick-okta still seeing this exact same issue as described by @patkovskyi :
The first approach leads to some users eventually sending expired JWT tokens to the backend
and by @nryoung :
We are also facing this issue and it is maddening. It only happens intermittently, but when it does the user is in a weird state and all network requests fail because their token is indeed invalid BUT the Okta SDK does not auto renew their token nor does it redirect users to login again. Sometimes having the user logout manually fixes the issue. Sometimes we have to have the user clear local storage and cookies in order for them to be able to use our application again.
We are using @okta/[email protected]
and @okta/[email protected]
Looking though the code, I do see that calls to OktaAuth.isAuthenticated()
will only make the attempt to renew a token if the autoRenew
flag is set on the TokenManagerOptions
under OktaAuthOptions
. Not sure if this is captured anywhere in the docs for okta-react
?
okta/okta-auth-js documentation says this is the default option.
By default, the tokenManager will attempt to renew tokens before they expire
In any case, isAuthenticated()
is not aware of this default, since it reads directly from the tokenManager
config on OktaAuth options, which is undefined
if not explicitly set
@theseyi As Aaron mentioned above, no workaround is still needed with the latest version. Can you try the react sample to see if you still see the issue? The default config enables token auto renew, you can also set expireEarlySeconds to make the renew easier to observe.
If you still see issue with the sample app, please create a new one to track, the current thread is targeting to a retired version (3.x), the renew strategy has been changed in the latest version.
@shuowu Thanks for taking a look, I will set up the react sample. However, part of the problem is that it is intermittent as @nryoung mentioned.
However, a few quick questions on your response:
The default config enables token auto renew
(1)
OktaAuth.isAuthenticated()
doesn't seem to be aware of the default config. As per my comment:
Looking though the code, I do see that calls to OktaAuth.isAuthenticated() will only make the attempt to renew a token if the autoRenew flag is set on the TokenManagerOptions under OktaAuthOptions
This is the code I'm referring to which is at current HEAD on the mainline branch master. autoRenew
and autoRemove
are both undefined
, is that expected?
const { autoRenew, autoRemove } = this.options.tokenManager || {};
if (accessToken && this.tokenManager.hasExpired(accessToken)) {
accessToken = null;
if (autoRenew) {
(2)
you can also set expireEarlySeconds
This is just at development time right? since:
NOTE expireEarlySeconds option is only allowed in the DEV environment (localhost). It will be reset to 30 seconds when running in environments other than DEV.
created related issue here @shuowu https://github.com/okta/okta-auth-js/issues/924