authts / react-oidc-context

Lightweight auth library based on oidc-client-ts for React single page applications (SPA). Support for hooks and higher-order components (HOC).
MIT License
658 stars 64 forks source link

Callback for loading existing credentials? #398

Open alexandroid opened 2 years ago

alexandroid commented 2 years ago

(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]);
pamapa commented 2 years ago

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

alexandroid commented 2 years ago

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?

pamapa commented 2 years ago

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

alexandroid commented 2 years ago

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.

alolis commented 1 year ago

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

alexandroid commented 11 months ago

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