auth0 / auth0-react

Auth0 SDK for React Single Page Applications (SPA)
MIT License
887 stars 256 forks source link

Log in and get access token using Auth0 React SDK and Authorization Code Flow with PKCE leads to infinite loop #567

Closed pingpongdoctor closed 1 year ago

pingpongdoctor commented 1 year ago

Checklist

Description

I have implemented the Auth0 React SDK along with the Authorization Code Flow with PKCE to obtain an access token in my React Single Page Application (SPA) for making API calls to my protected backend routes.

I chose the Authorization Code Flow with PKCE because SPAs are considered public apps, not confidential ones. Therefore, we cannot obtain an access token using the standard Authorization Code Flow.

When I use the loginWithRedirect() function, everything works smoothly after users are authenticated. However, after implementing the Authorization Code Flow with PKCE, which involves generating a link for user authentication and authorization with a code verifier and code challenge, users are redirected repeatedly to the redirect URL. Furthermore, the redirected links contain different values for the code query parameter each time.

Here is the link of the Authorization Code Flow with PKCE

Below is my code and the screenshot that demonstrates the problem.

Reproduction

When using the Authorization Code Flow with PKCE, I have to log in, and the isAuthenticated and user states retrieved from the useAuth0 hook are displayed in the Chrome browser console every time the home page is reloaded due to the infinite redirecting loop.

I've noticed that all the states are consistently either false or undefined, indicating that users are not being correctly authenticated. Consequently, this leads to the re-generation of the authorization link.

Additional context

Auth0Provider setup

import { Auth0Provider } from "@auth0/auth0-react";

import React from "react";

export const Auth0ProviderComponent = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  // const navigate = useNavigate();

  const domain = import.meta.env.VITE_AUTH0_ISSUER_BASE_URL;
  const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
  const redirectUri = import.meta.env.VITE_BASE_FRONTEND_URL;

  if (!(domain && clientId && redirectUri)) {
    return null;
  }

  return (
    <Auth0Provider
      domain={domain}
      clientId={clientId}
      authorizationParams={{
        redirect_uri: redirectUri,
      }}
    >
      {children}
    </Auth0Provider>
  );
};

main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Auth0ProviderComponent } from "./components/Auth0ProviderComponent.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <Auth0ProviderComponent>
      <App />
    </Auth0ProviderComponent>
  </React.StrictMode>
);

App.tsx

import { useEffect, useState } from "react";
import "./App.css";
import { useAuth0 } from "@auth0/auth0-react";
import CryptoJS from "crypto-js";

function App() {
  const { isAuthenticated, isLoading, user } = useAuth0();

  function base64URLEncode(input: any) {
    const words = CryptoJS.enc.Utf8.parse(input);
    const base64 = CryptoJS.enc.Base64.stringify(words);

    return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  }

  function sha256(input: any) {
    const words = CryptoJS.enc.Utf8.parse(input);
    const hash = CryptoJS.SHA256(words);

    return hash.toString(CryptoJS.enc.Base64);
  }

  useEffect(() => {
    if (!isLoading && !isAuthenticated && !user) {
      //Create verifier
      const randomBytes = CryptoJS.lib.WordArray.random(32); // 32 bytes
      const verifier = base64URLEncode(
        randomBytes.toString(CryptoJS.enc.Base64)
      );

      // Use the previously generated verifier to create the challenge code
      const challenge = base64URLEncode(sha256(verifier));

      //Generate link to authenticate and authorize users
      const config = {
        response_type: "code",
        client_id: import.meta.env.VITE_AUTH0_CLIENT_ID,
        code_challenge: challenge,
        code_challenge_method: "S256",
        redirect_uri: import.meta.env.VITE_BASE_FRONTEND_URL,
        audience: import.meta.env.VITE_AUTH0_CLIENT_AUDIENCE,
        scope: "read:user-resources",
      };
      const generatedLink = `https://${
        import.meta.env.VITE_AUTH0_ISSUER_BASE_URL
      }/authorize?${new URLSearchParams(config).toString()}`;

      //Use the link
      window.location.href = generatedLink;
    }

    if (!isLoading && isAuthenticated && user) {
      alert("successfully log in");
    }
  }, [isAuthenticated, isLoading, user]);
  return (
    <>
      <h1>Home page</h1>
    </>
  );
}

export default App;

Infinite loop in which users are redirected to this redirect uri with different values of the code query.

image image image

When I log isAuthenticated and user states, they are always false and undefined respectively.

image

I am very grateful for your helps. It has been quite a long time that I have been stuck with this bug. Thanks a lot

auth0-react version

2.2.1

React version

18.2.0

Which browsers have you tested in?

Chrome

frederikprijck commented 1 year ago

Thanks for reaching out. Our SDK already uses the authorization code flow with pkce, so there is no need for you to manually do this. Even more so, doing this manually will conflict with our SDK.

When I use the loginWithRedirect() function, everything works smoothly after users are authenticated. However, after implementing the Authorization Code Flow with PKCE, which involves generating a link for user authentication and authorization with a code verifier and code challenge, users are redirected repeatedly to the redirect URL

Using loginWithRedirect is the correct way, and it uses authorization code flow with pkce.

I recommend implementating our SDK as per https://auth0.com/docs/quickstart/spa/react, and rely on our SDK to do the authorization code flow with pkce internally for you.

pingpongdoctor commented 1 year ago

Thanks for your help. I fixed it.