auth0 / auth0-spa-js

Auth0 authentication for Single Page Applications (SPA) with PKCE
MIT License
917 stars 361 forks source link

Next js login @auth0/auth0-spa-js not working with loginWithRedirect while loginWithPopup works well #889

Closed tiwarivijay123 closed 2 years ago

tiwarivijay123 commented 2 years ago

Problem

When trying to use loginWithRedirect using https://github.com/auth0/auth0-spa-js first time it successfully logging me in and takes to onboard page means window.location.origin after login when I'm trying to login next time it just reloads the page and taking me back again and again while loginWithPopup works well.

What was the expected behavior?

Expected behaviour is after login it should take me to dashboard with token & user details.

Reproduction

Environment

TrueWill commented 2 years ago

I just saw something similar - unsure if it's the same bug or not.

We upgraded from version 1.10.0 to 1.20.1, so I would not expect any breaking changes (assuming people follow semver). I didn't see anything impactful in the changelog.

But now when calling loginWithRedirect (identical code other than the library version) and passing a redirect_uri, it's redirecting to a different URL and resulting in a 404.

Using more-or-less vanilla JS on this project (Backbone). Browser Chrome.

stevehobbsdev commented 2 years ago

We do try to follow semver wherever possible. If you can share your barebones project, I'd be happy to take a look. Also see if you can replicate the problem using our playground app (clone this repo and run npm install && npm start and test out the flow using our tenant details to rule out a tenant config issue.

TrueWill commented 2 years ago

The local playground app appears to work with Login redirect / callback, but again I verified that this broke our app changing nothing but the version of the library. Reverting to the older version fixed the issue. I can't share our many tens of thousands of lines of proprietary code split across multiple repositories, and it would take significant time to create a minimal repro.

The simplified version is that we call:

client = new Auth0Client(options);
accessToken = await client.getTokenSilently();
user = client.user;
idToken = await client.getIdTokenClaims();
// ...
client.loginWithRedirect({
  redirect_uri: '...',
  appState: {
    conversationUrl: '...',
    sessionId: id,
  },
  screen_hint: 'login',  // can also be login_hint depending on SSO status
});

Again, none of this changed. Also with the same Auth0 configuration this all works with the older library version.

stevehobbsdev commented 2 years ago

Do you require a different redirect_uri for different scenarios when calling loginWithRedirect or is it basically constant?

TrueWill commented 2 years ago

redirect_uri is based on modifying the current Window.location. In theory we might be able to set it once at client creation time, but the amount of QA effort to check every possible client scenario is significant.

Can you restore the original SDK behavior?

stevehobbsdev commented 2 years ago

I don't think we've changed anything specific in this area that would be causing your issue, but I've yet to deep-dive into it. Once I've had a chance to try and reproduce it this week, I'll come back to you.

tiwarivijay123 commented 2 years ago

I tried it's older version as well https://github.com/auth0/auth0-spa-js/releases but nothing seem to work. after successfully login it just come to back with undefined user detail and does nothing I'm using this version its latest but I tried older version as well but it is same

Screenshot 2022-03-29 at 10 57 41 AM

this is code which I'm trying for loginWithRedirect

Screenshot 2022-03-29 at 10 59 57 AM Screenshot 2022-03-29 at 10 54 06 AM
stevehobbsdev commented 2 years ago

@tiwarivijay123 can you check your tenant logs in your Auth0 dashboard and see if there are any errors reported in there? Do you have any errors in your browser console relating to Auth0?

Also when you reload the page and nothing happens, does it eventually do something if you wait for over a minute?

stevehobbsdev commented 2 years ago

@tiwarivijay123 in your code sample, you can't simply await loginWithRedirect and expect a result, as it will do a full-page redirect and state is lost. You need to call handleRedirectCallback when Auth0 redirects back to your application in order to get authentication results.

tiwarivijay123 commented 2 years ago

Hey @stevehobbsdev I tried handleRedirectCallback but still same

Screenshot 2022-03-29 at 7 05 42 PM Screenshot 2022-03-29 at 7 05 53 PM

andleRedirectCallback` also but does nothing for me

tiwarivijay123 commented 2 years ago

I checked my Auth0 logs also no issue there

Screenshot 2022-03-29 at 11 33 18 AM
stevehobbsdev commented 2 years ago

but does nothing for me

You're saying even after you call handleRedirectCalback after you come back from Auth0 and you're still unauthenticated when you call something like getUser or isAuthenticated?

I can't reproduce this scenario in my environment, either yours or @TrueWill so I'm going to have to see a repro in order to progress this. If you manage to come up with a small, reproducible sample that demonstrates the issue I'd be happy to take a look.

Mr-Fraser commented 2 years ago

Hi, I'm guessing the success is shown because there is a successful login but when another request is made after the redirect then we no longer have the token.. so the comment about using the "handleRedirectCallback" must be true, maybe we are not using it correctly @tiwarivijay123 ? image

Mr-Fraser commented 2 years ago

Here is more code to review @stevehobbsdev , any issues here please let us know..

function AuthProvider({ children }: AuthProviderProps) {
  const [open, setOpen] = useState(true);
  const router = useRouter();
  const [state, dispatch] = useReducer(reducer, initialState);
  const [isRegister, setIsRegister] = useState(false);
  async function getUserInfo(user: any, isAuthenticated: any) {
    try {
      const response = await axios.get('user/?filter={"where":{"email":"' + user.email + '"}}');
      console.log(response.data[0], 'user info');
      if (response.data.length == 0) {
        router.push({
          pathname: PATH_AFTER_SIGNUP,
          query: { returnUrl: router.asPath },
        });
      } else {
        return response.data[0];
      }
    } catch (error) {
      dispatch({
        type: Types.init,
        payload: { isAuthenticated: false, user: null },
      });
    }
  }

  const handleClickOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
  };
  const Transition = forwardRef(function Transition(
    props: TransitionProps & {
      children: React.ReactElement<any, any>;
    },
    ref: React.Ref<unknown>
  ) {
    return <Slide direction="up" ref={ref} {...props} />;
  });
  useEffect(() => {
    console.log(AUTH0_API.clientId, AUTH0_API.domain, 'Domain');
    const initialize = async () => {
      try {
        auth0 = new Auth0Client({
          client_id: '',
          domain: '',
          redirect_uri: window.location.origin,
          audience: '',
        });

        await auth0.checkSession();

        const isAuthenticated = await auth0.isAuthenticated();

        if (isAuthenticated) {
          const user = await auth0.getUser();
          const userInfo = await getUserInfo(user, isAuthenticated);
          // dispatch({
          //   type: Types.init,
          //   payload: { isAuthenticated, user: userInfo || null },
          // });
        } else {
          dispatch({
            type: Types.init,
            payload: { isAuthenticated, user: null },
          });
        }
      } catch (err) {
        console.error(err, 'erorr');
        dispatch({
          type: Types.init,
          payload: { isAuthenticated: false, user: null },
        });
      }
    };

    initialize();
  }, []);
  const login = async () => {
    try {
      await auth0?.loginWithRedirect();
    } catch (error) {
      let res = error.error_description.split(':');
      console.log(res[1], 'res 1');
      setIsRegister(true);
      return res;
    }
    if (isRegister == false) {
      const isAuthenticated = await auth0?.isAuthenticated();
      const user = await auth0.getUser();
      console.log(user, isAuthenticated, 'auth response');
      if (isAuthenticated) {
        const user = await auth0?.getUser();
        const accessToken = await auth0.getTokenSilently();
        axios.defaults.baseURL = '';
        axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
        axios.interceptors.request.use(
          (request) => {
            console.log(request);
            // Edit request config
            return request;
          },
          (error) => {
            console.log(error);
            return Promise.reject(error);
          }
        );

        axios.interceptors.response.use(
          (response) => {
            console.log(response);
            // Edit response config
            return response;
          },
          (error) => {
            console.log(error);
            return Promise.reject(error);
          }
        );
        const userInfo = await getUserInfo(user, isAuthenticated);
        localStorage.setItem('userId', userInfo.id);
        localStorage.setItem('accessToken', accessToken);
        dispatch({ type: Types.login, payload: { user: userInfo || null } });
      } else {
      }
    } else {
      const isAuthenticated = await auth0?.isAuthenticated();
      const user = await auth0.getUser();
      console.log(user, isAuthenticated, 'auth response');
      if (isAuthenticated) {
        const user = await auth0?.getUser();
        const accessToken = await auth0.getTokenSilently();
        localStorage.setItem('signupEmail', user?.email);
        window.localStorage.setItem('accessToken', accessToken);

        axios.defaults.baseURL = '';
        axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
        axios.interceptors.request.use(
          (request) => {
            console.log(request);
            // Edit request config
            return request;
          },
          (error) => {
            console.log(error);
            return Promise.reject(error);
          }
        );
        axios.interceptors.response.use(
          (response) => {
            console.log(response);
            // Edit response config
            return response;
          },
          (error) => {
            console.log(error);
            return Promise.reject(error);
          }
        );
      }
      // setIsRegister(false);
      router.push({
        pathname: PATH_AFTER_SIGNUP,
        query: { returnUrl: router.asPath },
      });
      // dispatch({ type: Types.Register, payload: { user: user } });
    }
  };

  const logout = () => {
    auth0?.logout();
    dispatch({ type: Types.logout });
  };
  console.log(state, 'state');

  return (
    <AuthContext.Provider
      value={{
        ...state,
        method: 'auth0',
        user: {
          additionalInfo: state?.user,
          id: state?.user?.sub,
          photoURL: state?.user?.avatar_signed_url,
          email: state?.user?.email,
          displayName: state?.user?.contact?.fullname,
          role: state?.user?.contact?.header,
        },
        login,
        logout,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}
tiwarivijay123 commented 2 years ago

Any success ?

TrueWill commented 2 years ago

Our company is planning to write up a minimal repro (probably not in Next.js though), but it may take some time. If anyone gets to it before then please share! Thanks!

Also for Next.js folks, I wonder if https://github.com/auth0/auth0-react would be a better option.

stevehobbsdev commented 2 years ago

@tiwarivijay123 I've got some feedback on your code sample I can share tomorrow.

@TrueWill we also have an SDK built specially for Next.js in case that's an even better fit: https://github.com/auth0/nextjs-auth0

tiwarivijay123 commented 2 years ago

Our company is planning to write up a minimal repro (probably not in Next.js though), but it may take some time. If anyone gets to it before then please share! Thanks!

Also for Next.js folks, I wonder if https://github.com/auth0/auth0-react would be a better option.

I used this but for some reason in case of loginWithRedirect it is same @TrueWill

stevehobbsdev commented 2 years ago

@tiwarivijay123 @Mr-Fraser So the thing to keep in mind is that there are two parts to the flow when using loginWithRedirect:

So this type of flow from your code sample will not work properly:

try {
  await auth0?.loginWithRedirect();
} catch (error) {
  let res = error.error_description.split(':');
  console.log(res[1], 'res 1');
  setIsRegister(true);
  return res;
}

// You'll never be authenticated without first calling `handleRedirectCallback`
const isAuthenticated = await auth0?.isAuthenticated();
const user = await auth0.getUser();
console.log(user, isAuthenticated, 'auth response');

What you should do is exit out of that flow immediately after calling loginWithRedirect, since a top-level redirect will be performed anyway. Note that when you're using loginWithPopup, what you have will work (and that's consistent with your issue report above) in that you can await loginWithPopup() and get the tokens back, since there's no top-level redirect.

The trick is detecting when you should call handleRedirectCallback when Auth0 redirects back to your app. In our React/Angular/Vue SDKs, we handle this internally essentially by checking for the presence of the code or state values on page load. Something like this modified example from Auth0 Vue:

if (
  (window.location.search.includes('code=') || window.location.search.includes('error=')) &&
  window.location.search.includes('state=')
) {
  const result = await auth0.handleRedirectCallback();

  // Remove the params from the URL
  window.history.replaceState({}, '', '/');

  // This should now work
  const user = await auth0.getUser();
  const isAuthenticated = await auth0.isAuthenticated();

  return result;
} else {
  await auth0.checkSession();
}

Note that this is where we also call checkSession if we're not coming from an Auth0 redirect (it's a normal page refresh or something like that).

Hopefully that gives you some food for thought as to how to manage this in your own application. What I would also suggest is running through one of our SPA quickstarts as an example to get a feel for how the flow should work in a cut-down environment. In particular the one for vanilla JavaScript as it lets you see the nuts and bolts of it a bit more (the React/Angular/Vue hide some of this for your convenience).

Hope that helps, and do let me know how you get on.

tiwarivijay123 commented 2 years ago

@tiwarivijay123 I've got some feedback on your code sample I can share tomorrow.

@TrueWill we also have an SDK built specially for Next.js in case that's an even better fit: https://github.com/auth0/nextjs-auth0

@stevehobbsdev can you let me know how I can get access token in this SDK https://github.com/auth0/nextjs-auth0 ?

tiwarivijay123 commented 2 years ago
// pages/api/products.js
import { getAccessToken, withApiAuthRequired } from '@auth0/nextjs-auth0';

export default withApiAuthRequired(async function products(req, res) {
  // If your Access Token is expired and you have a Refresh Token
  // `getAccessToken` will fetch you a new one using the `refresh_token` grant
  const { accessToken } = await getAccessToken(req, res, {
    scopes: ['read:products']
  });
  const response = await fetch('https://api.example.com/products', {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  });
  const products = await response.json();
  res.status(200).json(products);
});

I'm trying this code

tiwarivijay123 commented 2 years ago

any update for me ?

stevehobbsdev commented 2 years ago

@tiwarivijay123 Thanks for your patience. While we do try to respond in a timely fashion, you're unlikely to get a deep response over a weekend.

The code you have shown here is the example from Next.js Auth0 and so is our recommended way to get the access token. Does this not work for you? If you have any issues using that SDK, please raise it on the Next.js Auth0 repository.

If you're now using the Next.js Auth0 SDK, can this issue now be closed or do require further help?

tiwarivijay123 commented 2 years ago

Yeah ok thanks @stevehobbsdev I guess now you can close this issue