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

[iOS] storedStateManager is randomly not set, which creates sdk authenticated methods rejection #418

Open
Aeners opened this issue Apr 9, 2024 · 1 comment
Labels
bug Something isn't working

Comments

@Aeners
Copy link

Aeners commented Apr 9, 2024

Describe the bug?

I previously posted an issue #403 which relate the same behaviour, but I think that I understand the issue more in depths, so I prefered filling up a new issue.

Hello, the issue we encounter occurs on iOS devices only.
After some time, our users get "random" disconnections, they can use the app for a few days without any issue and suddenly they are logged out.

This seems to be due to okta-react-native sdk where getAccessToken and refreshTokens methods are throwing an unexpected error.
The message thrown inside the error is:

User is not authenticated, cannot perform the specific action

I took a look at the sdk iOS native code, which lead me to OktaSdkBridge.swift file.
In both getAccessToken and refreshTokens function declaration, I found out that that error is thrown when the storedStateManager is not set.

So if my digging is right, what could cause the storedStateManager to be unset?

Note that when this occurs, the token is still valid! I've been able to use the access token outside of the SDK scope and retrieve a successful response.

What is expected to happen?

Understand why the SDK methods getAccessToken and refreshTokens throw an error.
Could we have a wrong configuration of the SDK so the storedStateManager gets unset/reset?

What is the actual behavior?

Methods that use the storedStateManager variable are throwing an unexpected error "randomly".

User is not authenticated, cannot perform the specific action

Reproduction Steps?

We created a minimal project in order to narrow down as much as possible the issue. It is a bit tricky to give you access to it so I did my best below to describe the key points this project uses.

App initialisation

During the first render of the App, we create the okta config using the createConfig method.
This lands in a useEffect hook at the entrypoint of our App.

const oktaConfig: OktaConfig = {
  clientId: CLIENT_ID,
  redirectUri: REDIRECT_URI,
  endSessionRedirectUri: END_SESSION_REDIRECT_URI,
  discoveryUri: DISCOVERY_URI,
  scopes: ['openid', 'profile', 'offline_access'],
  requireHardwareBackedKeyStore: false,
};

const App = () => {
  const [appIsReady, setAppIsReady] = useState(false);

  useEffect(() => {
    createConfig(oktaConfig).finally(() => {
      setAppIsReady(true);
    });
  }, []);

  return (
    <FantasticWrapper>
     {appIsReady ? <GreatAppNavigation /> : <LoadingScreen />}
    </FantasticWrapper>
  );
}

Sign in screen

This screen is a simple authentication form, asking for user credentials.
On submit the app is calling the signIn method with crendentials filled in:

signIn({ username: email, password });

Using tokens for requests

From now on, we make requests through a GraphQL client.
Right before sending a request, the graphQL client calls getAccessToken method, and pass along the access token with the request.
In case the token expires, our BE responds with a 401 Unauthorized error, which triggers an error link of our graphQL client.
In this error link, we call the refreshTokens method, and then retry the request passing along the newly refreshed access token in it.
Now in case, the refreshTokens method throws an error, we force the disconnection of the user.

const refreshAndReturnAccessToken = async () => {
  try {
    const refreshTokensResponse = await refreshTokens();
    return refreshTokensResponse.access_token;
  } catch {
    // Shutting down session if unable to refresh tokens
    forceAppLogOut();
    return null;
  }
};

export const attachTokenLink = (() =>
  setContext(async (_, headers) => {
    let accessToken = '';

    try {
      const getAccessTokenResponse = await getAccessToken();
      accessToken = getAccessTokenResponse.access_token;
    } catch {
      accessToken = await refreshAndReturnAccessToken();
    }

    return {
      headers: {
        ...headers,
        authorization: `Bearer ${accessToken}`,
      },
    };
  }))();

export const errorLink = onError(({ graphQLErrors, operation, forward }) => {
  if (graphQLErrors) {
    for (let error of graphQLErrors) {
      if (error.extensions?.status === 401) {
        return fromPromise(
          refreshAndReturnAccessToken().then(token => {
            if (!token) {
              return null;
            }
            const previousHeaders = operation.getContext().headers;
            operation.setContext({
              headers: {
                ...previousHeaders,
                authorization: `Bearer ${tokens.access_token}`,
              },
            });
            return tokens.access_token;
          }))
          .filter(Boolean)
          .flatMap(() => forward(operation));
        }
      }
    }
});

export const client = new ApolloClient({
  link: from([errorLink, attachTokenLink, httpLink]),
});

getAccessToken and refreshTokens throw unexpected errors randomly

After being logged in, after some time the getAccessToken and the refreshTokens method throws an error with this message:

User is not authenticated, cannot perform the specific action

Additional Information?

Here is the previous issue #403 I posted, at that time we were more wondering if the token was wrongly compared by the device. Though the discussion might help so I leave the link of it.

SDK Version

2.12.0

Build Information

No response

@leandigital
Copy link

Seeing the same problem in our implementation. Has there been any progress here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants