Alternative to next_auth and other frameworks for authentication in Next.js
Asked Answered
H

1

8

I am trying to develop a flexible authentication system on Next.js that can use a Spring (Java) API backend. The endpoints function perfectly using Postman. The API also provides its own JWT. I want to sign in registered users using the API endpoint. This also means I need a way to use the JWT from the server to authenticate the user trying to sign in.

Following the documentation for both Next_auth and iron-sessions has been very confusing. The API works fine. Next_auth in particular seems to provide limited support for this type of authentication.

I've researched quite a few posts, tutorials and even posted this question. This question comes the closest to what I'm trying to understand, but it deals with a post sign in state and the process looks a bit confusing. This question seems to say that it's quite complicated to perform custom authentication on Next and it's always best to use frameworks.

Am I missing something here or is it very complicated to get Next js to work with external APIs and JWT? I don't need the full stack functionality that Next has to offer. I also don't want to be forced to authenticate through Google, Twitter, FB, etc.

I need something like this, which was created using React and uses REST API endpoints to sign in registered users and manage the respective user sessions.

  var myHeaders = new Headers();
  myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
  
  var urlencoded = new URLSearchParams();
  urlencoded.append("username", enteredEmail);
  urlencoded.append("password", enteredPassword);

  var requestOptions = {
    method: 'POST',
    headers: myHeaders,
    body: urlencoded,
    redirect: 'follow'
  };

  fetch(API_LOGIN_URL, requestOptions)
    .then((res) => {
      setIsLoading(false);
      if (res.ok) {
        return res.json();
      } else {
        return res.json().then((data) => {
          let errorMessage = 'Authentication failed!';
          throw new Error(errorMessage);
        });
      }
    })
    .then((data)=> {
      authCtx.login(data.access_token);
      const processedData = JSON.stringify(data);
      console.log("Admin status "+ processedData);
     for(let i = 0; i < processedData.length; i++) {
            if(processedData.includes("ROLE_SUPER_ADMIN")) {
            console.log("Found Admin"); 
            authCtx.adminAccess(true);
          } 
          if(processedData.includes("ROLE_USER")) {
            console.log("Found User");
            break;
          }
          else {
            console.log("Not Found");
          }
    }})
    .catch((err) => {
      alert(err.message);
    });
}
};

  return (
  <section className={classes.auth}>
  <h1>{isLogin ? 'Login' : 'Sign Up'}</h1>
  <form onSubmit={submitHandler}>
    <div className={classes.control}>
      <label htmlFor='email'>Your Email</label>
      <input type='email' id='email' required ref={emailInputRef} />
    </div>
    <div className={classes.control}>
      <label htmlFor='password'>Your Password</label>
      <input type='password' id='password' required ref={passwordInputRef} />
    </div>
    <div className={classes.actions}>
      {!isLoading && <button>{isLogin ? 'Login' : 'Create Account'}</button>}
      {isLoading && <p>Sending request</p>}
      <button
        type='button'
        className={classes.toggle}
        onClick={switchAuthModeHandler}
      >
        {isLogin ? 'Create new account' : 'Login with existing account'}
      </button>
    </div>
  </form>
  </section>
  );
  };

 export default AuthForm;

I'd like to do something similar in Next.js without working according to the rules of frameworks / libraries like next_auth.

I would really appreciate any guidance, (advice, tutorials, etc) that explain how to use a post method to an API endpoint to look up a username and password.

I'd also like to know how to use the JWT generated from the API to complete the process and authenticate the user. I can put this part in another question. For this question I'd be happy even if I know how to sign in by looking up username and password, using the API endpoints I've described. In Next.js, I've only seen authentication done using frameworks like Next_auth or iron-sessions. I haven't seen the the type of custom authentication methods you'd find in React (described above). Therefore, I'd like to know:

Do we have to use Next_auth or iron-sessions for authentication? Are there any examples of custom Next js authentication methods that don't rely on these frameworks and play nicely with backend APIs and JWT such as Spring?

Thanks in advance for any help.

Heisler answered 10/9, 2022 at 18:21 Comment(3)
I'm trying to do something similar with a Node.js backend and passport-local(username/password) / session based authentication. I found this article which involves using a higher order component to protect routes: mikealche.com/software-development/…Boric
Thanks a lot Dream_Cap! This looks really useful. This article basically suggests using React Context to manage auth states. I'll try doing that today and update the question. I'd also like to know how you get on with the Node project and if passport might be another way to go.Heisler
Anytime! I'm still trying to wrap my head around that article, but this is the small node app I made: github.com/capozzic1/portfolio-node-appBoric
H
10

With great help from Dream_Cap, who directed me to a relevant article and his own node.js code, the answer is that it is totally possible to write a custom authentication method without relying on any frameoworks such as next_auth.

The crux of the solution is that Next js can use React Context, as a a Higher Order Component (HOC), to hold the authentication state and persist changes in the user session accordingly. This is a somewhat different approach from using the [...nextuth].js approach which is designed to catch all requests.

This alternative method basically means you can use almost the same approach as you would in a normal React application, but slightly modified to a Next.js context:

let logoutTimer;
let initialToken;
let initialAdminToken;

const AuthContext = React.createContext({
  token: '',
  admintoken: '',
  isLoggedIn: false,
  isAdmin: false,
  login: (token) => { },
  adminAccess: (admintoken) => { },
  logout: () => { },
});

const calcTimeRemaining = (expirationTime) => {
  const currentTime = new Date().getTime();
  const adjExpireTime = new Date(expirationTime).getTime();
  const remaingDuration = adjExpireTime - currentTime;

  return remaingDuration;
}

export const AuthContextProvider = (props) => {
  const authCtx = useContext(AuthContext);
  const isAdmin = authCtx.isAdmin;

  const [token, setToken] = useState(initialToken);
  const [admintoken, setAdminToken] = useState(initialAdminToken);

  const userIsLoggedIn = !!token;
  const userHasAdmin = !!admintoken;


  useEffect(() => {
    initialToken = localStorage.getItem('token');
    initialAdminToken = localStorage.getItem('admintoken');
    if(initialAdminToken !== initialToken) {
      setToken(initialToken);
     
    } else {
      setToken(initialToken);
      setAdminToken(initialAdminToken);
    }

  }, [initialToken, initialAdminToken]);



  const logoutHandler = () => {
    setToken(null);
    setAdminToken(null);
    localStorage.removeItem('token');
    localStorage.removeItem('admintoken');
  };

  const loginHandler = (token) => {
    if(admintoken == null) {
      setToken(token);
    
    localStorage.setItem('token', token);
    } else {
      setToken(token);
      localStorage.setItem('token', token);
      setAdminToken(token);
      localStorage.setItem('admintoken', token);
    }
    // const remainingTime = calcTimeRemaining(expirationTime);
    setTimeout(logoutHandler, 300000);
    
  };

  const adminTokenHandler = (admintoken) => {
   setAdminToken(admintoken);
   localStorage.setItem('admintoken', admintoken);
  }

  const contextValue = {
    token: token,
    admintoken: admintoken,
    isAdmin: userHasAdmin,
    isLoggedIn: userIsLoggedIn,
    adminAccess: adminTokenHandler,
    login: loginHandler,
    logout: logoutHandler,
  };

  return (
    <AuthContext.Provider value={contextValue}>
      {props.children}
    </AuthContext.Provider>
  );
};


export default AuthContext;

The login form:

 const AuthForm = () => {
  const emailInputRef = useRef();
  const passwordInputRef = useRef();
  const [isLoading, setIsLoading] = useState(false);
  const [isAdmin, setIsAdmin] = useState(false);
  const router = useRouter();

  const authCtx = useContext(AuthContext);


  const submitHandler = (event) => {

    event.preventDefault();

    const enteredEmail = emailInputRef.current.value;
    const enteredPassword = passwordInputRef.current.value;


    var myHeaders = new Headers();
    myHeaders.append("Content-Type", "application/x-www-form-urlencoded");

    var urlencoded = new URLSearchParams();
    urlencoded.append("username", enteredEmail);
    urlencoded.append("password", enteredPassword);

    var requestOptions = {
      method: 'POST',
      headers: myHeaders,
      body: urlencoded,
      redirect: 'follow'
    };

    fetch(API_LOGIN_URL, requestOptions)
      .then(async (res) => {
        setIsLoading(false);
        if (res.ok) {
          return res.json();
        } else {
          const data = await res.json();
          let errorMessage = 'Authentication failed!';
          throw new Error(errorMessage);
        }
      })
      .then((data) => {
        authCtx.login(data.access_token);
        router.replace('/');
        const processedData = JSON.stringify(data);
        for (let i = 0; i < processedData.length; i++) {
          if (processedData.includes("ROLE_SUPER_ADMIN")) {
            console.log("Found Admin");
            authCtx.adminAccess(data.access_token);
              
          } else {
            console.log("Found User");
            authCtx.adminAccess(null);
          }
          
         
        }
      })
      .catch((err) => {
        alert(err.message);
      });

  };

  return (
    <section className={classes.auth}>
      <h1>Login</h1>
      <form onSubmit={submitHandler}>
        <div className={classes.control}>
          <label htmlFor='email'>Your Email</label>
          <input type='email' id='email' required ref={emailInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='password'>Your Password</label>
          <input type='password' id='password' required ref={passwordInputRef} />
        </div>
        <div className={classes.actions}>
          {!isLoading && <button>Login</button>}
          {isLoading && <p>Sending request</p>}
        </div>
      </form>
    </section>
  );
};

 

To protect the routes:

const ProtectRoute = ({ children }) => {
  const authCtx = useContext(AuthContext);
const isLoggedIn = authCtx.isLoggedIn;

if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname == '/') {
      return <HomePage />;
    } else {
      if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname !== '/auth') {
        return <RestrictedSection />;
      } 
      else {
   return children;
    } 
  }

}

export default ProtectRoute;

Finally, the route protection is wrapped around the main _app.js file:

function MyApp({ Component, pageProps }) {



// const ProtectedPages = dynamic(()=> import ('../store/ProtectRoute'));

  return (
   <AuthContextProvider>
    
        <Layout>
        <ProtectRoute> 
          <Component {...pageProps} />
          </ProtectRoute>
        </Layout>
      
    </AuthContextProvider>

  )
};

export default MyApp
Heisler answered 27/9, 2022 at 6:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.