Skip to content

CRITICAL: isAuthenticated State Never Updates When Refresh Token Expires - App Stuck in Authenticated State #5

@Sam-Lam-Varadise

Description

@Sam-Lam-Varadise

🚨 CRITICAL Bug Report

Severity: HIGH - App gets stuck in authenticated state, preventing route guards from working

Description

When the refresh token expires, isAuthenticated never changes to false, leaving the app stuck in an authenticated state. This breaks protected routes and auth guards. Additionally, users encounter a JSON parse error from getUserInfo.

User Impact

  • Route guards don't trigger - Users remain on protected screens even though they're logged out
  • Expo Router protected routes fail - Navigation doesn't switch to login screen
  • Apps get stuck in an inconsistent auth state
  • JSON parse errors appear in logs

Error Message

ERROR  [OIDC-AUTH PROVIDER] get user info failed [SyntaxError: JSON Parse error: Unexpected end of input] 
SyntaxError: JSON Parse error: Unexpected end of input

Root Cause

Two critical issues in the provider's state change handler:

Issue 1: Unhandled Exception Blocks State Update

In packages/react-native-oidc-auth-core/src/provider/oidc-auth/oidc-auth.tsx (lines 46-56):

const subscription = instance.onStateChanged(state => {
  tracingLog.info(`[OIDC-AUTH PROVIDER] state changed: ${state}`);

  if (state === EventState.TOKEN_EXPIRED) {
    instance.updateToken();  // ❌ NOT AWAITED, NO ERROR HANDLING!
  } else if (state === EventState.INIT_COMPLETED) {
    setIsInitializing(true);
  }

  setIsAuthenticated(instance.isAuthenticated());  // ❌ NEVER REACHED IF updateToken() THROWS!
});

What happens:

  1. TOKEN_EXPIRED event fires
  2. instance.updateToken() is called
  3. If refresh token is null/expired, updateToken() throws an error (line 427 in oidc-auth.ts)
  4. Exception bubbles up and prevents setIsAuthenticated() from being called
  5. App stays stuck with isAuthenticated = true

Issue 2: Missing REFRESH_ERROR Event Handler

When updateToken() doesn't throw but the refresh fails:

  1. clearToken() is called, setting authenticated = false internally
  2. REFRESH_ERROR event is emitted (line 487 in oidc-auth.ts)
  3. Provider doesn't handle REFRESH_ERROR event - only handles TOKEN_EXPIRED and INIT_COMPLETED
  4. State is never updated in the provider
  5. App stays stuck with isAuthenticated = true

Code Location

File: packages/react-native-oidc-auth-core/src/provider/oidc-auth/oidc-auth.tsx

Problem 1 (lines 46-56): No error handling for updateToken()
Problem 2 (lines 46-56): Missing handler for REFRESH_ERROR event

Expected Behavior

  • When refresh token expires, isAuthenticated should reliably change to false
  • Route guards should trigger and redirect to login
  • No unhandled errors should crash the state update

Actual Behavior

  • isAuthenticated never changes when refresh token expires
  • App stuck in authenticated state
  • Protected routes remain accessible
  • JSON parse errors appear
  • Route navigation doesn't trigger

Proposed Solution

Fix both issues with proper error handling and event listening:

useEffect(() => {
  const subscription = instance.onStateChanged(state => {
    tracingLog.info(`[OIDC-AUTH PROVIDER] state changed: ${state}`);

    if (state === EventState.TOKEN_EXPIRED) {
      // Properly handle updateToken errors
      instance.updateToken().catch(e => {
        tracingLog.error('[OIDC-AUTH PROVIDER] updateToken failed', e);
        // Force state update even if updateToken throws
        setIsAuthenticated(instance.isAuthenticated());
      });
    } else if (state === EventState.INIT_COMPLETED) {
      setIsInitializing(true);
    } else if (state === EventState.REFRESH_ERROR) {
      // Handle refresh error event - state update is critical here
      tracingLog.info('[OIDC-AUTH PROVIDER] refresh failed, updating auth state');
    }

    // Always update authentication state
    setIsAuthenticated(instance.isAuthenticated());
  });

  tracingLog.debug('[OIDC-AUTH PROVIDER] Initialization provider');

  instance
    .initState()
    .catch(e =>
      tracingLog.error('[OIDC-AUTH PROVIDER] initialization failed', e),
    );

  return subscription;
}, []);

Additional improvement - Clean up user state:

useEffect(() => {
  const getUserInfo = async () => {
    let userInfo: User | null = null;
    try {
      userInfo = await instance.getUserInfo();
      setUserFetchSuccess(true);
      setUserFetchError(false);
    } catch (e) {
      setUserFetchError(true);
      setUserFetchSuccess(false);
      tracingLog.error('[OIDC-AUTH PROVIDER] get user info failed', e);
    }

    setUser(userInfo);
  };

  if (isAuthenticated) {
    getUserInfo();
  } else {
    // Clean up user state when logged out
    setUser(null);
    setUserFetchSuccess(false);
    setUserFetchError(false);
  }
}, [isAuthenticated, instance]);

Optionally - Better error messages in getUserInfo implementations:

In both oidc-auth-bare.ts and oidc-auth-expo.ts, wrap the getUserInfo calls in try-catch to provide clearer error messages about token expiration.

Impact

  • Severity: CRITICAL - Completely breaks authentication flow
  • Frequency: Occurs every time a refresh token expires
  • User Experience: App becomes unusable - users can't be logged out, route guards fail
  • Security: Users may access protected content when they shouldn't

Reproduction Steps

  1. Log in to app with OIDC auth
  2. Wait for refresh token to expire (or manually delete it from storage)
  3. Trigger token refresh (e.g., make an API call)
  4. Observe: isAuthenticated remains true, route guards don't trigger
  5. App stuck on protected screens despite being logged out

Environment

  • Library: react-native-oidc-auth
  • Affected platforms: iOS, Android, Expo
  • Affected implementations: Both bare React Native and Expo
  • Context: Any app using route guards or protected routes (Expo Router, React Navigation)

Related Code References

  • packages/react-native-oidc-auth-core/src/provider/oidc-auth/oidc-auth.tsx (line 46-56: event handler)
  • packages/react-native-oidc-auth-core/src/oidc-auth/oidc-auth.ts (line 427: updateToken throws)
  • packages/react-native-oidc-auth-core/src/oidc-auth/oidc-auth.ts (line 481: clearToken call)
  • packages/react-native-oidc-auth-core/src/oidc-auth/oidc-auth.ts (line 487: REFRESH_ERROR emission)
  • packages/react-native-oidc-auth-core/src/oidc-auth/enum/event-state.ts (EventState enum)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions