How to use next auth to authenticate through a custom Spring API library and endpoints
Asked Answered
P

1

4

I am a newbie to Next.js and moving my front end from React to Next (v4). I've already developed a back end using Spring which connects to a MySQL database hosted on Azure. The API is fully tested and functional on Postman and a React front end. The API includes endpoints allowing for authentication. The API also generates a JWT token.

Moving the front end from React to Next js

In trying to move the front end from React, authentication is the first problem being faced. I chose to try to next_auth, but there seems to be some problems in implementing next_auth using username and password credentials.

Next auth for authenticating with Spring / Java REST APIs

The above leads me to ask if Next auth is even suitable for authenticating with Spring APIs in the first place? Or if a custom method (ie: standard React) would be better? The Next auth documentation seems to prefer using built-in methods like Google, Twitter, etc, with less support for custom credentials methods.

Similar questions on SOF

I didn't find many similar questions on this specific use case. This is the only question I found that talks about Next auth and Spring APIs. There are no answers to the question.

The closest thing to my problem is this question. It says the problem is about the configuration of the JWT token and callback. I followed the directions but it didn't solve the problem.This question is about connecting Next auth to a Spring boot API. This question is generally about how to configure and use Credentials provider.

Based on all of this, I tried the below:

React code (including Postman signature)

The following code in React works fine with the API. I have also extracted the Postman signature in the fetch method.

const AuthForm = () => {

const emailInputRef = useRef();
const passwordInputRef = useRef();
const [isLogin, setIsLogin] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);

const authCtx = useContext(AuthContext);

const switchAuthModeHandler = () => {
setIsLogin((prevState) => !prevState);
};

const submitHandler = (event) => {
event.preventDefault();

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

// optional: Add validation

setIsLoading(true);

if (isLogin) {

  // Postman signature here

  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;

Next.js Application Code

[...nextauth].js

  export default NextAuth({
  session: {
  strategy: "jwt",
  secret: process.env.SECRET,
  maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  providers: [
  CredentialsProvider({
  name: 'credentials',
  credentials: {
    Username: {label: "Username", type: "text", placeholder: '[email protected]'},
    Password: {label: "Password", type: "password"}
  },
  async authorize(credentials, req) {

     // extracted from Postman 
  var myHeaders = new Headers();
  myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
  
  
  var urlencoded = new URLSearchParams();
  urlencoded.append("Username", credentials.Username);
  urlencoded.append("Password", credentials.Password);

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

const user = fetch(API_LOGIN_URL, {
  requestOptions
  })

console.log("Credentials are: " + credentials.Username)
if (user) {

// Any object returned will be saved in `user` property of the JWT
return user
} else {
// If you return null or false then the credentials will be rejected
return null
// You can also Reject this callback with an Error or with a URL:
// throw new Error('error message') // Redirect to error page
// throw '/path/to/redirect'        // Redirect to a URL
 }
  
 },

  })],

callbacks: {
 async session({ session, token, user }) {
  session.user.id = token.id;
  session.accessToken = token.accessToken;
  return session;
 },
async jwt({ token, user, account, profile, isNewUser }) {
if (user) {
  token.id = user.id;
}
if (account) {
  token.accessToken = account.access_token;
}
  return token;
},

}
}

)

Authform.js (login form)

import { useState, useRef, useContext } from 'react';
import classes from './AuthForm.module.css';
import { signIn } from 'next-auth/react';
import { API_LOGIN_URL } from '../Constants';
import { redirect } from 'next/dist/server/api-utils';

  function AuthForm () {
   const emailInputRef = useRef();
   const passwordInputRef = useRef();


 async function submitHandler (event) {
 event.preventDefault();

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

  const result = await signIn('credentials', {
   redirect: false,
   username: enteredEmail,
   password: enteredPassword,
   callbackUrl: `${window.location.origin}`

  });  
  console.log("The email " + enteredEmail);

    console.log("Result is: " + result.error);
 }
    
return (
  <section className={classes.auth}>
       <h1>Login</h1>
    <form onSubmit={submitHandler}>
    <div className={classes.control}>
      <label htmlFor='username'>Your Email</label>
      <input type='email' id='username' 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}>
      <button>Login</button>
      <button
        type='button'
        className={classes.toggle}
     >
        
      </button>
    </div>
  </form>
 </section>
 );


 }
   export default AuthForm;

Errors

On the front end

When actually logging in, I get no errors. If I refresh the browser after restarting the application I get:

[next-auth][warn][NO_SECRET] https://next-auth.js.org/warnings#no_secret [next-auth][error][JWT_SESSION_ERROR] https://next-auth.js.org/errors#jwt_session_error decryption operation failed { message: 'decryption operation failed', stack: 'JWEDecryptionFailed: decryption operation failed\n' +

The back end

The login actually does reach the back end point (when monitoring the API terminal), but it gives null for the user name and password values.

Main questions

  1. Is Next auth the right framework to authenticate to a Spring API or is it better to write a custom authentication method (or even stick to using React)?
  2. What is wrong with the above code?

Thank you!

Pilau answered 6/9, 2022 at 6:49 Comment(1)
Hi jay tai. Did you manage to figure out your main questions?Kickstand
A
1

In the credentials you have mentioned username but in the form it's email?

Can you update the async authorize(credentials, req) code to the below & try -

const payload = {
    email: credentials.email, // make sure this field is mapped correctly
    password: credentials.password,
};

const res = await fetch(API_LOGIN_URL, {
    method: 'POST',
    body: JSON.stringify(payload),
    headers: {
        'Content-Type': 'application/json',
        'Accept-Language': 'en-US',
    },
});

const user = await res.json();

Also the signIn method's signature seems to be incorrect -

Syntax

signIn('credentials', { redirect: false, password: 'password' })
signIn('email', { redirect: false, email: '[email protected]' })

Usage

const result = await signIn('credentials', {
    headers: myHeaders,
    body: urlencoded,
   redirect: false
  });  

Please update to

const res = await signIn('credentials', {
 redirect: false,
 email: values.email,
 password: values.password,
 callbackUrl: `${window.location.origin}`, // if required
});
Alicaalicante answered 6/9, 2022 at 8:13 Comment(5)
Thanks AG. I updated my code based on your answer. I made everything consistent in terms of username instead of email. I even tried hardcoding values in the credential mapping for the username and password. I also tried to hardcode the same values in the Authform. Unfortunately none of this has so far solved any of the errors I described. The front end is still sending null values to the API. I am still getting exactly the same errors. Any suggestions on my updated code and most recent attempts? Thanks again.Pilau
What is the postman & react front end request signature which are giving successful result? Please share.Alicaalicante
I updated the question with the React code and Postman signature. Let me know if it is clear. Thank you.Pilau
Still can't see what object you are passing to fetch or postman. In the fetch(API_LOGIN_URL, requestOptions) - what does console.log(requestOptions) prints?Alicaalicante
Sorry. I accidentally forgot to include it. I have updated the code and it should be there now.Pilau

© 2022 - 2024 — McMap. All rights reserved.