react-oidc-context icon indicating copy to clipboard operation
react-oidc-context copied to clipboard

Callback for loading existing credentials?

Open alexandroid opened this issue 2 years ago • 6 comments

(This can be either documentation request or a new feature request, I am not sure)

My react app uses Redux to store user info, and I am trying to set it from the addUserLoaded callback and from onSigninCallback config callback. It seems to work , but only during initial signin, when no valid credentials are present.

When I reload the application, neither of those callbacks appear to be triggered. It goes straight to the authenticated state.

Is there another or more universal callback available which can be used to "catch" when user credentials are either loaded or found in the local storage? I want to avoid trying to deduce it from useAuth().user. In the past we used Amplify and did period checks for this but it seems hacky and now impossible to do without direct access to UserManager (I saw https://github.com/authts/react-oidc-context/issues/331).


My callback in the AuthProvider config:

const oidcStateStore = new oidcClient.WebStorageStateStore({
  store: window.localStorage,
});

const oidcConfig: OidcAuthProviderProps = {
      authority: 'https://...',
      client_id: 'client1',
      redirect_uri: window.location.origin,
      loadUserInfo: true,
      userStore: oidcStateStore,
      automaticSilentRenew: false,
      onSigninCallback: (user: oidcClient.User | void): void => {
        window.history.replaceState(
          {},
          document.title,
          window.location.pathname,
        );
        // set user info in Redux store
      },
    };

My addUserLoaded callback:

const auth = useAuth();

useEffect(() => {
  return auth.events.addUserLoaded((user: User) => {
    // set user info in Redux store
  });
}, [auth.events]);

alexandroid avatar Jun 01 '22 16:06 alexandroid

Behind useAuth() is a reducer. You can directly trigger on auth.user in your useEffect. See https://github.com/authts/react-oidc-context/blob/main/src/AuthProvider.tsx#L175, this is doing already what you need...

pamapa avatar Jun 02 '22 06:06 pamapa

Triggering on auth.user does not seem work either:

useEffect(() => {
  return auth.events.addUserLoaded((user: User) => {
    // never called for subsequent page refresh
  });
}, [auth.user]);

I've added logging statement in reducer() and I can see that it's only called with INITIALIZED action type and never with USER_LOADED in this case.

I even tried making my own manager and hook into addUserLoaded() there - still does not get triggered.

class CustomUserManager extends oidcClient.UserManager {
  constructor(settings: oidcClient.UserManagerSettings) {
    super(settings);
    this.events.addUserLoaded((user) => {
      console.info(`does not happen`);
    });
  }
}

const oidcConfig: OidcAuthProviderProps = {
   ...
   implementation: CustomUserManager,
   ...
}

It looks like the culprit may be here: https://github.com/authts/react-oidc-context/blob/main/src/AuthProvider.tsx#L160 it calls userManager.getUser() which tries to load from the storage and in this particular case it suppresses callback notifications (false passed as the second argument to _events.load()): https://github.com/authts/oidc-client-ts/blob/e735d83454f7abbefeeb4aaab899647b8655c74e/src/UserManager.ts#L124

In fact, it looks like UserManager triggers "user loaded" callback only from _signInEnd(), _useRefreshToken() and _remokeInternal() - none of which are being triggered if user credentials already exist in the storage.

I managed to make it work by implementing overriding the default getUser() behavior but it seems hacky as hell:

class CustomUserManager extends oidcClient.UserManager {
  private _user: oidcClient.User | null;

  constructor(settings: oidcClient.UserManagerSettings) {
    super(settings);
    this._user = null;
  }

  // Follows the parent implementation except forces raising
  // "load user" events when user is loaded for the first time.
  public async getUser(): Promise<oidcClient.User | null> {
    const logger = this._logger.create('getUser');
    const user = await this._loadUser();
    if (user) {
      logger.info('user loaded');
      let raiseEvents = false;
      if (this._user === null) {
        this._user = user;
        raiseEvents = true;
        // set user info in Redux store
      }
      this._events.load(user, raiseEvents);
      return user;
    }

    this._user = null;
    logger.info('user not found in storage');
    return null;
  }
}

Do you see other / better way to do it?

alexandroid avatar Jun 07 '22 00:06 alexandroid

In order to initiate the auth process you need to call signinRedirect somewhere in your code...

pamapa avatar Jun 07 '22 06:06 pamapa

Yes, I do that below, in the same component where I call useAuth()/useEffect():

if (auth.isAuthenticated) {
  return <>{children}</>;
}

if (!auth.error && auth.activeNavigator === undefined && !auth.isLoading) {
  console.info('Initiating signinRedirect()');
  auth.signinRedirect();
}

// show "authenticating..." spinner or an error message, depending on
// auth.error and auth.isLoading...

It is triggered only during the initial load. Once the access tokens are in the local storage, signinRedirect() is not called - and I don't expect it to.

alexandroid avatar Jun 14 '22 18:06 alexandroid

@alexandroid , did you ever managed to find a more elegant way to do this instead of overriding the getUser ?

alolis avatar Sep 22 '23 09:09 alolis

@alolis No =\ I am not sure why this issue was closed.

alexandroid avatar Oct 05 '23 05:10 alexandroid