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 manager auto renew not working #1164

Open
jameslessen opened this issue Apr 1, 2022 · 33 comments
Open

Token manager auto renew not working #1164

jameslessen opened this issue Apr 1, 2022 · 33 comments
Labels

Comments

@jameslessen
Copy link

Describe the bug?

The access token expires without renewal despite having refresh token lifetime set to unlimited. The refresh token and access token expire and the user is redirected to login again. This happens even when refresh token rotation is disabled

What is expected to happen?

The access token should be periodically refreshed until the expiration of the refresh token

What is the actual behavior?

Access Token Lifetime: 5 minutes
Refresh Token Lifetime: unlimited
Refresh token rotation is enabled
Login
09:27:04.420 tokenManager added event emitted for accessToken and refreshToken, both with the same expiresAt value
09:31:33.998 tokenManager expired event emitted for refreshToken
09:31:34.011 Request made to /oauth2/default/v1/token to fetch new tokens using refresh_token_a
09:31:35.005 tokenManager expired event emitted for accessToken
09:31:35.791 tokenManager added event emitted for refreshToken
09:31:35.797 Request made to /oauth2/default/v1/token to fetch new tokens using refresh_token_a(Returns the same refreshToken as the request in step 4, but a different accessToken)
09:31:35.005 tokenManager expired event emitted for accessToken
09:31:36.729 OKTA-AUTH-JS:updateAuthState: Event:added Status:canceled
09:36:05.001 tokenManager expired event emitted for refreshToken
09:36:05.565 Okta Error Event:

09:36:05.565 OAuthError: The client specified not to prompt, but the user is not logged in.
    Babel 2
    w 1.chunk.js:250780
    n CustomError.ts:13
    Be 1.chunk.js:251791
    n OAuthError.ts:14
    sn handleOAuthResponse.ts:21
    sn handleOAuthResponse.ts:56
    promise callback*sn handleOAuthResponse.ts:54
    un getToken.ts:117
    promise callback*un/< getToken.ts:115
    promise callback*un getToken.ts:72
    cn getWithoutPrompt.ts:21
    fn renewToken.ts:18
    value PromiseQueue.ts:44
    value PromiseQueue.ts:26
    value PromiseQueue.ts:25
    Yn TokenManager.ts:260
    promise callback*Yn TokenManager.ts:252
    Xn TokenManager.ts:321
    e TokenManager.ts:398
    emit index.js:36
    a TokenManager.ts:43
    a TokenManager.ts:96
    sentryWrapped helpers.ts:87
    setTimeout handler*./node_modules/ 1.chunk.js:278516
    Ln TokenManager.ts:95
    Hn TokenManager.ts:134
    Xn/t.renewPromise[r]< TokenManager.ts:294
    promise callback*Xn TokenManager.ts:321
    e TokenManager.ts:398
    emit index.js:36
    a TokenManager.ts:43
    a TokenManager.ts:96
    sentryWrapped helpers.ts:87
    setTimeout handler*./node_modules/ 1.chunk.js:278516
    Ln TokenManager.ts:95
    f TokenManager.ts:194
    Jn TokenManager.ts:221
    e browser.ts:417
    u runtime.js:45
    _invoke runtime.js:274

09:36:05.998 tokenManager expired event emitted for accessToken
09:36:06.340 Okta Error Event (Same as above)
09:36:06.536 Redirect to login page

Reproduction Steps?

Set up an application with the following properties
Access Token Lifetime: 5 minutes
Refresh Token Lifetime: unlimited

Sign in and wait 10 minutes

I added some event listeners to the token manager for better visibility

oktaAuth.tokenManager.on('expired', (a, b, c) => {
  console.group('Okta Expired Event');
  console.log(a);
  console.log(b);
  console.log(c);
  console.groupEnd();
});
oktaAuth.tokenManager.on('error', (a, b, c) => {
  console.group('Okta Error Event');
  console.log(a);
  console.log(b);
  console.log(c);
  console.groupEnd();
});

Okta Config:

export const oktaConfig: OktaAuthOptions = {
  issuer: `${process.env...}/oauth2/default`,
  clientId: process.env...,
  redirectUri: `${window.location.origin}/login/callback`,
  scopes: ['openid', 'email', 'offline_access'],
  tokenManager: {
    autoRenew: true,
  },
  devMode: true,
};

SDK Versions

"@okta/okta-react": "5.1.2"
"@okta/okta-auth-js": "4.9.0"

Execution Environment

Firefox 98.0.2 (64-bit)

Additional Information?

Posting this here since the behavior seems to be related to the token manager and our application does not have any custom token refresh behavior

@jameslessen jameslessen added the bug label Apr 1, 2022
@arvindkrishnakumar-okta
Copy link

Thanks for the post!

Someone from our team will review this soon.

@aarongranick-okta
Copy link
Contributor

@jameslessen Please update to the latest version of AuthJS. From the network response you posted, it is clear that the client is falling back to using an iframe/cookie method to refresh tokens (getWithoutPrompt) rather than using the refresh tokens. This was a known issue which was fixed in version 5.2.1. We recommend updating to the latest version to take advantage of all current fixes.

@nitrique
Copy link

nitrique commented Nov 2, 2022

Same here ok 7.0.1 !

@jaredperreault-okta
Copy link
Contributor

@nitrique can you provide a code snippet?

@nitrique
Copy link

nitrique commented Nov 2, 2022

export const oktaAuth = new OktaAuth({
  redirectUri: `${ window.location.origin }/login/callback`,
  clientId: window.config.OKTA_CLIENT_ID,
  pkce: true,
  issuer: `${ issuer?.endpoint ?? authIssuers[0].endpoint }/oauth2/default`,
  // Enable this to get auth event messages in console
  devMode: false,
});

oktaAuth.start();

export async function initAuthentication(): Promise<UserClaims | null> {
  const tokenParams: TokenParams = {
    scopes: [ "groups", "openid", "profile", "email" ],
  };

  const accessToken: AccessToken = (await oktaAuth.tokenManager.get(
    "accessToken",
  )) as AccessToken;

  if (!accessToken) {
    await oktaAuth.token.getWithRedirect(tokenParams);
  }

  return accessToken?.claims;
}

initAuthentication();

Some code has been omitted. The okta service never trigger auto renew, we have to do the following:

const interval = setInterval(async () => {
      // Renew 60 seconds before expiry
      if (claims?.exp && getUnixTime(new Date()) > (claims.exp - 60)) {
        oktaAuth.tokenManager.renew("accessToken");
      }
    }, 15e3);

@jaredperreault-okta
Copy link
Contributor

@nitrique How are you handling the redirect back to your app?

@nitrique
Copy link

nitrique commented Nov 2, 2022

@jaredperreault-okta can you be more specific ?

Redirect only occur on login. On token renew we don't have redirect (classic OIDC flow).

If you are talking about first redirect:

if (oktaAuth.isLoginRedirect()) {
      try {
        oktaAuth
          .handleLoginRedirect()
          .then(() => location.href = location.origin);
      } catch (e) {
        // log or display error details
      }
    } else {
      location.href = location.origin;
    }

@jaredperreault-okta
Copy link
Contributor

@nitrique can you try

oktaAuth.tokenManager.on('expired', console.log);

I'm curious if the renew requests are failing or whether the renew process is never triggered

@nitrique
Copy link

nitrique commented Nov 3, 2022

it write "accessToken" on console.

@jaredperreault-okta
Copy link
Contributor

Can you check the network tab to see if any network requests are being made (when renew triggers)

and add the following log line

oktaAuth.tokenManager.on('error', console.log);

to see if token renewal is throwing any errors

@nitrique
Copy link

nitrique commented Nov 3, 2022

Nothing.

I don't have the time to debug the lib sorry, I've implemented my own timer which work ;)

@karhatsu
Copy link

karhatsu commented Nov 9, 2022

Not sure if this is the same issue but I'm seeing the token renewal working in most of the cases but then quite often not (let's say 60-80% success rate). Here's my config:

new OktaAuth({
  issuer: '...',
  clientId: '...',
  redirectUri: `${window.location.origin}/signin`,
  tokenManager: {
    expireEarlySeconds: 59 * 60, // I'm using this to get token refresh once a minute
  },
  devMode: true,
})

I'm using @okta/okta-react and have a hook like this:

  const onTokenRenewal = useCallback((key: string, newToken: Token) => {
    console.log(new Date().toISOString(), 'new token', key)
    // here I have the code to utilize the new token but omitting it now
  }, [])

  const onTokenExpire = useCallback((key: string) => {
    console.log(new Date().toISOString(), 'expired', key)
  }, [])

  const onTokenError = useCallback((err: Error) => {
    console.log(new Date().toISOString(), 'error', err)
  }, [])

  const onTokenRemove = useCallback((key: string) => {
    console.log(new Date().toISOString(), 'removed', key)
  }, [])

  const onTokenAdded = useCallback((key: string) => {
    console.log(new Date().toISOString(), 'added', key)
  }, [])

  useEffect(() => {
    console.log(new Date().toISOString(), 'start')
    oktaAuth.start()
    oktaAuth.tokenManager.on('renewed', onTokenRenewal)
    oktaAuth.tokenManager.on('expired', onTokenExpire)
    oktaAuth.tokenManager.on('error', onTokenError)
    oktaAuth.tokenManager.on('removed', onTokenRemove)
    oktaAuth.tokenManager.on('added', onTokenAdded)
    return () => {
      oktaAuth.tokenManager.off('renewed')
      oktaAuth.tokenManager.off('expired')
      oktaAuth.tokenManager.off('error')
      oktaAuth.tokenManager.off('removed')
      oktaAuth.tokenManager.off('added')
      oktaAuth.stop()
    }
  }, [oktaAuth, onTokenRenewal, onTokenExpire, onTokenRemove, onTokenAdded, onTokenError])

When it works correctly, I'm seeing logs like this:

2022-11-09T11:18:45.484Z start
OKTA-AUTH-JS:updateAuthState: Event:undefined Status:canceled
OKTA-AUTH-JS:updateAuthState: Event:undefined Status:emitted
2022-11-09T11:19:21.003Z expired idToken
2022-11-09T11:19:21.007Z expired accessToken
2022-11-09T11:19:21.814Z removed idToken
KTA-AUTH-JS:updateAuthState: Event:added Status:canceled
2022-11-09T11:19:21.815Z added idToken
2022-11-09T11:19:21.815Z new token idToken
OKTA-AUTH-JS:updateAuthState: Event:removed Status:canceled
2022-11-09T11:19:21.817Z removed accessToken
OKTA-AUTH-JS:updateAuthState: Event:added Status:canceled
2022-11-09T11:19:21.817Z added accessToken
2022-11-09T11:19:21.817Z new token accessToken
OKTA-AUTH-JS:updateAuthState: Event:undefined Status:emitted
2022-11-09T11:19:21.826Z removed idToken // for some reason id and access tokens are renewed three times

A minute later there's another similar logging sequence.

When I see that the renewal process works, I refresh the browser page and wait. If it turns out that the renewal process doesn't work, the logs look like this:

2022-11-09T11:20:40.922Z start
OKTA-AUTH-JS:updateAuthState: Event:undefined Status:canceled
OKTA-AUTH-JS:updateAuthState: Event:undefined Status:emitted
2022-11-09T11:21:22.010Z expired idToken
2022-11-09T11:21:22.011Z expired accessToken
// and nothing after this

In the latter case there are no calls to the /oauth2/v1/token API as in the first case. As you can see from above, I have an error listener but that logs nothing either.

I'm using these versions meaning I have the latest ones:

    "@okta/okta-auth-js": "^7.0.1",
    "@okta/okta-react": "^6.7.0",

I have also tried this version but the result is the same:

  useEffect(() => {
    console.log(new Date().toISOString(), 'start')
    oktaAuth.start().then(() => {
      oktaAuth.tokenManager.on('renewed', onTokenRenewal)
    })
    ...

@jaredperreault-okta
Copy link
Contributor

@nitrique @karhatsu did you notice your respective issues prior to upgrading to 7.0.1?

I've added a ticket to our next sprint to investigate this

Internal Ref: OKTA-549676

@karhatsu
Copy link

karhatsu commented Nov 9, 2022

I have built the app on top of 7.0.1 so I don't have experience from the prior versions.

@nitrique
Copy link

nitrique commented Nov 9, 2022

@karhatsu It works for me with the following on React:

export const oktaAuth = new OktaAuth({
  redirectUri: `${ window.location.origin }/login/callback`,
  clientId: window.config.OKTA_CLIENT_ID,
  pkce: true,
  issuer: `${ issuer?.endpoint ?? authIssuers[0].endpoint }/oauth2/default`,
  // Enable this to get auth event messages in console
  devMode: false,
  cookies: {
    secure: true,
  },
  storageManager: {
    token: {
      storageTypes: [
        "cookie",
      ],
    },
    cache: {
      storageTypes: [
        "cookie",
      ],
    },
    transaction: {
      storageTypes: [
        "cookie",
      ],
    },
  },
  tokenManager: {
    expireEarlySeconds: 15,
    autoRemove: true,
    autoRenew: true,
    secure: true,
  },
});
export const tokenRenewListener = (key: string) => {
  oktaAuth.tokenManager.renew(key);
};

(I agree this function is a pass-through)

In auth context:

useEffect(() => {
    oktaAuth.start();

    // .... truncated code (handle auth state check and redirect) .....

    oktaAuth.tokenManager.on("expired", tokenRenewListener);

    return () => {
      stopOktaAuth();
      oktaAuth.tokenManager.off("expired", tokenRenewListener);
    };
  }, []);

HINT : oktaAuth.start() is needed to monitor token state

-> Take care of token storage, this lib stores by default on localStorage, which is a big CVE as any browser's plugin can access it (see https://auth0.com/docs/secure/security-guidance/data-security/token-storage), prefer secure cookies.

-> My advice is to DON'T use Okta React lib, which has to much dependencies like react-navigation (old version) and can differ from your implementation

-> Second advice, find an OpenIDConnect generic lib, it's better than auth providers libs which lead to vendor lock-in and most of the time has a better maintainance, if you look at this lib in exemple, it still uses "var" keyword, in 2022...

@karhatsu
Copy link

@nitrique Thank you for the hints! I've been trying it out with as default Okta settings as possible, so been using local storage for now. Not sure if it has any impact for this case.

Probably the biggest difference to your code is the usage of 59 minutes in tokenManager.expireEarlySeconds. First of all, this way I can run way more test cases than with the values close to 0 seconds. On the other hand, it could also have an impact to the bug; maybe the possible race condition is related to renewing the tokens too often?

@nitrique
Copy link

@karhatsu tons of articles explain what you shouldn't use local storage like this one https://dev.to/rdegges/please-stop-using-local-storage-1i04 , but it's up to you :)

The token should last the biggest time frame that you can accept if access token is stolen (I'm not talking of refresh token), for critical applications 1 minute can be good, but generally 30 minutes is a good start.

Take care of not emitting too long access token, this is the purpose of refresh token. In my client's company, I requested to set the token expiration to 5 minutes which I think is good, even if it's not a critical application. Token renewal is free :)

To answer your question, OpenIDConnect doesn't mention a limit of renewal as I know, but Okta devs can have implemented it differently

@karhatsu
Copy link

I appreciate your advice @nitrique. My point here are not the security details but providing the Okta team information how to figure out the issue in the code. I would be quite surprised if it's related to the storage type. Also the 59 minutes early expiration is not something I'd use normally (and wouldn't be even possible according to Okta docs), it's just for easier debugging the problem.

@denysoblohin-okta
Copy link
Contributor

@karhatsu
I am not able to reproduce your issue, but I assume it's related to leader election service.
When you open 2+ tabs with your app, one one tab should be a leader and perform token auto renewal.
Even if you use only 1 tab, it should be self-elected as a leader.
If election fails for some reason, token renew process will not be triggered.

(We use broadcast-channel for leader election)

Could you please answer following question to help me investigate your issue further?

  • What browser are you using?
  • Did you notice you have the issue when 2+ tabs are opened? If yes, do you see same issue on other tabs?
  • When you have the issue, can you please debug the values of:
    • oktaAuth.serviceManager.services.get('leaderElection').isLeader()
    • oktaAuth.serviceManager.services.get('autoRenew').isStarted()
    • oktaAuth.serviceManager.services.get('leaderElection').elector.isDead
    • oktaAuth.serviceManager.services.get('leaderElection').elector.hasLeader
  • If you open broadcast-channel test page (1 tab) and perform page refresh several times, do you see any errors in console? Does console message "is leader" show up every time?

@karhatsu
Copy link

Sorry for taking so long to respond. I'm using Chrome, haven't tested with other browsers.

When the renewal doesn't work, it logs these values:

2022-12-15T08:08:16.005Z expired idToken
isLeader false
isStarted false
isDead false
hasLeader true

When it does work, then it logs like this:

2022-12-15T08:12:44.219Z expired idToken
isLeader true
isStarted true
isDead false
hasLeader true

I have never tried this before with having two tabs open. Now when I tested it and when it worked, the first tab logged the latter output and the second the former. Both were still able to update the token.

I tried your test page several times and all the time it logged is leader.

@denysoblohin-okta
Copy link
Contributor

Version 7.2.0 will include the fix for your issue.

@karhatsu
Copy link

I tried with 7.2.0 but unfortunately I'm still seeing similar behaviour.

@denysoblohin-okta
Copy link
Contributor

Could you please also debug these values with auth-js 7.2.0 when token is expired:

{
 syncStorageCanStart: oktaAuth.serviceManager.services.get('syncStorage').canStart(),
 syncStorageStarted: oktaAuth.serviceManager.services.get('syncStorage').isStarted(), 
 autoRenewCanStart: oktaAuth.serviceManager.services.get('autoRenew').canStart(),
 autoRenewStarted: oktaAuth.serviceManager.services.get('autoRenew').isStarted(),
 isLeader: oktaAuth.serviceManager.services.get('leaderElection').elector.isLeader,
 hasLeader: oktaAuth.serviceManager.services.get('leaderElection').elector.hasLeader,
 type: oktaAuth.serviceManager.services.get('leaderElection').elector.broadcastChannel.method.type
}

From your logs above

isLeader false
hasLeader true

it seems like there are 2 tabs opened and current one is not a leader, but from your comment I understand that there is only 1 tab opened, so it can be a bug with leader election.

Could you please also contact support@okta.com to help us gathering more information to investigarte?

@alexfedosov
Copy link

I've looked at the issue in auth-js 7.2.0 and figured that starting oktaAuth service does not always start AutoRenewService while canStart() returns true - that has been tested in Chrome (111.0.5563.64) and Safari 16.3 (18614.4.6.1.6) with no plugins in two different projects. I've also confirmed the problem exists with one and several tabs.

Currently this seems to be a reliable workaround

oktaAuth
    .start()
    .then(() => {
        const autoRenew = oktaAuth.serviceManager.getService('autoRenew')
        if (autoRenew && !autoRenew.isStarted()) {
            return autoRenew.start()
        }
    })

@denysoblohin-okta
Copy link
Contributor

denysoblohin-okta commented Apr 5, 2023

@alexfedosov When autoRenew.start() resolves a promise, autoRenew service might not start yet. It requires some time to elect a leader tab and then start autoRenew service on a leader tab. But if doesn't start after a while (several seconds), then it looks like a problem same as @karhatsu has.

@alexfedosov @karhatsu

  • Do you use Next.js or some other framework with SSR?
  • Do you call oktaAuth.stop() somewhere in your app code?
  • Are you sure you have only 1 instance of OktaAuth on a page?
    I can reproduce an issue if call oktaAuth.stop() in the cleanup function of userEffect .

Internal ref: OKTA-597615

@alexfedosov
Copy link

@denysoblohin-okta

  • We don't use SSR
  • oktaAuth.stop() is ofc in the cleanup of react's useEffect but we have this effect in the root component, so essentially it is never called
  • There is only single instance of oktaAuth

Basically we have this

import { useOktaAuth } from '@okta/okta-react'

export const useAccessTokenRenewal = () => {
  const { oktaAuth } = useOktaAuth()

  useEffect(() => {
    oktaAuth.start()
    return () => {
      oktaAuth.stop()
    }
  }, [oktaAuth])
}

and then there are several places where we again use useOktaAuth() hook to verify auth state and get user token, but this should not create extra OktaAuth instances, right?

@denysoblohin-okta
Copy link
Contributor

Right, should not create extra instances. But:

  1. If you use <Security> from '@okta/okta-react', it already calls oktaAuth.start(), so start can be called twice which could be a cause of an issue.
  2. If you use <React.StrictMode /> in React 18, it calls your effect twice. So start + stop + start can occur.

You can debug what's going on with start and stop calls with this code example

const origStart = OktaAuth.prototype.start;
const origStop = OktaAuth.prototype.stop;
OktaAuth.prototype.start = function() {
  console.log('>>> OktaAuth start');
  return origStart.apply(this);
};
OktaAuth.prototype.stop = function() {
  console.log('>>> OktaAuth stop');
  return origStop.apply(this);
};

@alexfedosov
Copy link

alexfedosov commented Apr 11, 2023

We certainly have it called twice due to the <Security> component; thank you for the hint.

Regarding the start + stop + start sequence, it looks like a fairly common command chain. Shouldn't the Okta-auth handle it?

@denysoblohin-okta
Copy link
Contributor

@alexfedosov
Could you please try the branch from #1398 in your project and check if it solves your issue?

@alexfedosov
Copy link

@denysoblohin-okta removing oktaAuth.start() (while keeping Security component) seems to fix it, I can also verify the branch in the next few days and let you know

@igloo12
Copy link

igloo12 commented Jul 20, 2023

I am having this issue with the latest okta-react the on "renewed" event is never called but the "added" event works

     // never called
    oktaAuth.tokenManager.on("renewed", (key, newToken, oldtoken) => {
        console.log(`renewed token`)
        if ('accessToken' in newToken) {
            updateToken(newToken.accessToken);
        }
    })
export const createOktaAuth = () => {
    return new OktaAuth({
        services: {
            autoRenew: true,
            autoRemove: true,
            syncStorage: true,
        },
      ...
    })
}

@RyAndrew
Copy link

This leadership election bug is present on mobile ios safari v 17.2.1

I get isLeader = false

Using the testing tool https://pubkey.github.io/broadcast-channel/ I do not have this bug

@jaredperreault-okta
Copy link
Contributor

@RyAndrew Do you mind filing a new issue?

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

No branches or pull requests

10 participants