Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token renewal fails silently for some users, some of the time (3.0.4/3.0.8, third-party cookies allowed) #15

Open
patkovskyi opened this issue Oct 16, 2020 · 22 comments

Comments

@patkovskyi
Copy link

patkovskyi commented Oct 16, 2020

Our setup:

  1. SPA, authorization code flow with PKCE.
  2. Okta and app domains are different.
  3. 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).
  4. 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
  1. idToken and accessToken expiration time is set to 60 minutes in Okta.
  2. As a workaround attempt, we call authService.getIdToken() before each request to our backend (we then send idToken to the backend). This does not seem to help.
  3. We collect frontend logs via a separate unauthenticated endpoint. I subscribed to tokenManager's expired, renewed, and error 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?

@patkovskyi patkovskyi changed the title Token renewal fails silently for some users, some of the time (3.0.4, third-party cookies allowed) Token renewal fails silently for some users, some of the time (3.0.4/3.0.8, third-party cookies allowed) Oct 21, 2020
@patkovskyi
Copy link
Author

patkovskyi commented Oct 21, 2020

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?

@man0a
Copy link

man0a commented Oct 29, 2020

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

@aarongranick-okta
Copy link
Collaborator

@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?

@aarongranick-okta
Copy link
Collaborator

@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: okta/okta-oidc-js#744 (comment)

@patkovskyi
Copy link
Author

patkovskyi commented Oct 29, 2020

@aarongranick-okta I'm a bit confused, in the okta/okta-oidc-js#744 (comment) 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 from authState
  • 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.).

@nryoung
Copy link

nryoung commented Nov 17, 2020

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 okta/okta-oidc-js#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.

@swiftone
Copy link
Contributor

@nryoung - do you have any information on what triggers this condition?

@nryoung
Copy link

nryoung commented Nov 17, 2020

@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: okta/okta-oidc-js#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.

@aarongranick-okta
Copy link
Collaborator

@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?

@nryoung
Copy link

nryoung commented Nov 18, 2020

@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.

@geoffreyaguero
Copy link

I'm also having this issue, I moved from authState.accessToken to authService.getAccessToken() but still get the same behavior.

@nryoung
Copy link

nryoung commented Dec 4, 2020

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.

@amacleay-cohere
Copy link

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>
  );
}

@amcdnl
Copy link
Contributor

amcdnl commented Apr 1, 2021

@amacleay-cohere - is this change still relevant?

@amacleay-cohere
Copy link

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 !

@amcdnl
Copy link
Contributor

amcdnl commented Apr 1, 2021

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.

@EricHedden
Copy link

@amcdnl has applying the tweak solved your problem?

@aarongranick-okta
Copy link
Collaborator

@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.

@theseyi
Copy link

theseyi commented Sep 1, 2021

@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

@shuowu
Copy link
Contributor

shuowu commented Sep 1, 2021

@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.

@theseyi
Copy link

theseyi commented Sep 1, 2021

@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.

@theseyi
Copy link

theseyi commented Sep 2, 2021

created related issue here @shuowu okta/okta-auth-js#924

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests