Git Product home page Git Product logo

Comments (6)

slax57 avatar slax57 commented on May 18, 2024 1

Again, thank you so much for this helpful additional info 🙂 .

Indeed this would deserve a new issue IMO.

I have a question though. If we set the logout URL to /logout (which internally will call msalInstance.logoutRedirect({ account })), does it not automatically remove the localStorage keys used for the cache for you?
IMO this should be the responsibility of the MSAL, RA should only call the lib's logout mehod at logout.
Did you give it a try?

from ra-auth-msal.

dnk8n avatar dnk8n commented on May 18, 2024

It is likely my API is being called with stale JWT before the frontend has called its acquireTokenSilent function. This means that by default the system performs a logout in response to the API's 401 error.

i.e. FE might be doing what it needs to, but too late. I am investigating a way for the 401 error to be caught and retried. See below code (which is unfortunately not yet working, but explains what I am tying):

Edit: Retracted code, because it is repeated in the following comment in better format.

from ra-auth-msal.

dnk8n avatar dnk8n commented on May 18, 2024

On further inspection, it seems the problem is only on the boundary of the expiry time (an edge case). Often the token refresh is working as expected. The rough debugging code that helped me work out what is going on is here in case anyone feels like digging deeper... but this is working for me.

In the rare case that the API is called before the token is refreshed, it seems to be covered (I am pretty fresh to javascript/react, so feedback welcome):

export const tokenRequest: SilentRequest = {
  scopes: ["User.Read"],
  forceRefresh: false,
};

export const myMSALObj = new PublicClientApplication(msalConfig);

const baseAuthProvider = msalAuthProvider({
  msalInstance: myMSALObj,
  loginRequest,
  tokenRequest,
  redirectOnCheckAuth: false
});

export const authProvider = {
  ...baseAuthProvider,
  checkError: async (error: { status: number; message: { body: string; }; }) => {
    if (error.status === 403) {
      return Promise.resolve();
    } else if (
      error.status === 401 &&
      error instanceof HttpError &&
      error.message === "JWT expired"
    ) {
      return Promise.resolve();
    }
    return baseAuthProvider.checkError(error);
  }
};

So basically in the above, we ensure that the frontend is OK with 403s and a 401 in the case that JWT is expired. The dataprovider needs to catch that error and retry so that the failed request brings the expected data the second time round... e.g

const acquireToken = async ({ msalInstance, tokenRequest }: MsalHttpClientParams) => {
  const authResult = await msalInstance.acquireTokenSilent({
    account: msalInstance.getActiveAccount() as AccountInfo,
    ...tokenRequest,
    scopes: tokenRequest!.scopes !== undefined ? tokenRequest!.scopes : [],
  });
  const id_token = authResult?.idToken;
  return { authenticated: !!id_token, token: `Bearer ${id_token}` };
};

const postgrestHttpClient = ({ msalInstance, tokenRequest }: MsalHttpClientParams) => async (url: string, options: Options = {}) => {
  // ...
  const user = await acquireToken({ msalInstance, tokenRequest });
  let onboardCalled = false;
  try {
    return await fetchUtils.fetchJson(url, { ...options, user });
  } catch (error) {
    if (
      error instanceof HttpError &&
      error.body &&
      error.body.message &&
      error.body.message.includes("JWT expired")
    ) {
      const user = await acquireToken(
        {
          msalInstance,
          tokenRequest: { ...tokenRequest, forceRefresh: true } as SilentRequest
        }
      );
      return await fetchUtils.fetchJson(url, { ...options, user });
    } else if {
    // ...
    }
    else {
      throw error;
    }
  }
};

from ra-auth-msal.

slax57 avatar slax57 commented on May 18, 2024

Thanks for the detailed report!
We'll look into it.

from ra-auth-msal.

dnk8n avatar dnk8n commented on May 18, 2024

Note: Edited the code that worked for me once last time as I realized it wasn't entering the if condition I expected it to, and forceRefresh is required to be true in this edge case.

from ra-auth-msal.

dnk8n avatar dnk8n commented on May 18, 2024

The solution implemented above, successfully handles an edge case identified (the window of time before token has expired according to the front-end and backend) by a try/catch, but one should probably allow a configurable tolerance, defaulting to ~10 seconds). I.e. refresh the token a few seconds before it is about to expire, so that the next API call is sure to receive a valid token. This doesn't necessarily have to be the front end's responsibility though, although easier to configure (so allow tolerance to be disabled too).

The following might need to be logged as a new issue, but is somewhat related:

In Azure AD, there is the following option:

Front-channel logout URL
This is where we send a request to have the application clear the user's session data. This is required for single sign-out to work correctly.

I don't think your tutorial mentions this. Maybe not so necessary by default because React-admin defaults to redirecting to /login. But probably good to set (edit, I found I didn't need to set it... but the information to clear users session data was helpful).

Apparently, it is the responsibility of the app to "clear the user's session data". Without doing this, in the case where you are automatically logged out (by edge case above!) or when you manually log out yourself, you won't be allowed to log in again unless you do something like (note the line with localStorage.removeItem):

export const CustomLoginPage = () => {
  const login = useLogin();
  const [loading, setLoading] = useSafeSetState(false);
  const notify = useNotify();
  const submit = () => {
    setLoading(true);
    for (const key in localStorage) {
      if (key.includes(import.meta.env.VITE_MSAL_CLIENT_ID)) {
        localStorage.removeItem(key);
      }
    }
    login({})
      .then(() => {
        setLoading(false);
      })
      .catch((error) => {
        setLoading(false);
        const errorMsg =
          typeof error === "string"
            ? error
            : error && error.message
              ? error.message
              : undefined;
        notify(errorMsg, {
          type: "error",
          messageArgs: {
            _: errorMsg,
          },
        });
      });
  };

I delete from localstorage because of my configuration:

const msalConfig: Configuration = {
  auth: {
    clientId: import.meta.env.VITE_MSAL_CLIENT_ID,
    authority: import.meta.env.VITE_MSAL_AUTHORITY,
    redirectUri: `${import.meta.env.VITE_APP_BASE_URI}/auth-callback`,
    navigateToLoginRequestUrl: false
  },
  cache: {
    cacheLocation: "localStorage"
  },
};

Note: navigateToLoginRequestUrl being false was needed, else odd behavior.

from ra-auth-msal.

Related Issues (5)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.