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
- 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)?
- What is wrong with the above code?
Thank you!