next-auth.session-token invalid signature on jwt.io - next.js

Did someone encounter similar errors when trying to decode your next-auth.session-token on jwt.io? I followed a video and tried copy my session token to jwt.io to see it, but all i get is an error saying "Invalid signature"
What did i miss here?
[...nextauth].tsx file
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
type: "credentials",
credentials: {},
authorize(credentials, req) {
const { email, password } = credentials as {
email: string;
password: string;
};
//Perform your login logic here
//Find out user from db
if (email !== "john#gmail.com" || password !== "1234") {
throw new Error("Invalid credentials");
}
//If everything is fine
return { id: "1234", name: "John Doe", email: "john#gmail.com" };
},
}),
],
pages: {
signIn: "/auth/signin",
},
};
export default NextAuth(authOptions);
Cookie image
jwt io image

Related

NextAuth with HttpOnly cookies from external API

Hi looking for a way to implement authentication and authorization with NextAuth and HttpOnly cookies from an external api.
So when you log in on the external api, you get the user info:
{
id: '',
email: '',
role: ''
}
and you also get an Authentication and Refresh token on the Set-Cookie header (with a max-age of 300s and 1w, same expirations are used on the token).
So to my understanding, I have to do the following:
In the authorize fn:
POST login, forward the Set-Cookies using res.setHeader
read out Authentication token and get exp (expiration in seconds)
return the accesTokenExp and the user data
JWT Callback:
on signin (when user is defined), add user to the token object
check if Date.now() is past accessToken expiry, if not return token else
get new accessToken in Set-Cookie and UserModel from api and forward it using res.setHeader
return token with user and new accesTokenExp
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { RestUserModel } from '../../../types/models';
import logger from '../../../utils/logger';
import { getData, postData } from '../../../utils/request';
import { getCookieValue, getTokenExpiryFromCookies } from '../../../utils/utils';
import jsonwebtoken from "jsonwebtoken"
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
interface UserToken extends RestUserModel {
accessTokenExpiry: number
}
const nextAuthOptions: NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse): NextAuthOptions => {
return {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email", placeholder: "jhon#doe.com" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
logger.info(`NextAuth authorize for user ${credentials?.email}`);
try {
const response = await postData<RestUserModel>('/auth/signin', {
body: {
email: credentials?.email,
password: credentials?.password,
},
})
logger.info("Authorization was successful!");
const cookies = response.headers['set-cookie'] || [];
console.log(cookies)
logger.info("cookies: " + cookies);
res.setHeader('Set-Cookie', cookies)
const accessToken = getCookieValue(cookies.join(','), 'Authentication') || '';
logger.info('token: ' + accessToken)
logger.info('tokenSecret: ' + process.env.NEXTAUTH_SECRET)
logger.info('cookie decoding: ' + JSON.stringify(jsonwebtoken.decode(accessToken), null, 2));
const accessTokenExpiry = (getTokenExpiryFromCookies(cookies.join(',')) || 0) * 1000;
return {
...response.data,
accessTokenExpiry,
}
} catch (error: any) {
logger.error(`NextAuth authorize error: ${error.message}`);
throw new Error(error)
}
}
})
],
callbacks: {
async signIn({ user }) {
if (!(user.role === "admin" || user.role === "manager")) return false
return true
},
async jwt({ token, user }) {
logger.info(`JWT User ${JSON.stringify(user, null, 2)}`);
logger.info(`JWT Token ${JSON.stringify(token, null, 2)}`);
if (user) {
token.user = user
}
const shouldrefreshtime = Math.round(((token.user as UserToken).accessTokenExpiry - 60 * 1000) - Date.now())
logger.info(`Token accessExpiry: ${(token.user as UserToken).accessTokenExpiry} - Date.now(): ${Date.now()}`)
logger.info(`Token accessExpiry - 60ms * 1000: ${(token.user as UserToken).accessTokenExpiry - 60 * 1000}`)
logger.info(`Token should refresh: ${shouldrefreshtime}`)
if (shouldrefreshtime > 0) return token;
try {
const response = await getData<RestUserModel>('/auth/refresh', {
cookies: 'Refresh=' + req.cookies['Refresh']
})
response.headers['set-cookie'] && res.setHeader('Set-Cookie', response.headers['set-cookie'])
const accessTokenExpiry = getTokenExpiryFromCookies((response.headers['set-cookie'] || []).join(','));
logger.debug(`JWT Refresh Response Cookies: ${JSON.stringify(response.headers['set-cookie'], null, 2)}`)
delete token.error;
return {
...token,
user: {
...response.data,
accessTokenExpiry: (accessTokenExpiry || 0) * 1000
}
}
} catch (error) {
logger.error(`JWT Callback ERROR: ${error}`)
return {
...token,
error: "RefreshAccessTokenError",
}
}
},
async session({ session, user, token }) {
console.log('session', { session, user, token })
return {
...session,
user: {
...session.user,
...token.user as UserToken,
},
}
}
},
session: {
strategy: "jwt"
},
events: {
async signOut() {
res.setHeader("Set-Cookie", [
"Authentication=deleted;Max-Age=0;path=/;",
"Refresh=deleted;Max-Age=0;path=/;"
]);
},
},
secret: process.env.NEXTAUTH_SECRET,
}
}
const Auth = (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
export default Auth;
There seem to be an issue with the Refresh cookie being undefined.
But now I wonder if there isn't an easier way. I seem to be doing twice the work. Also is the use of NextAuth a good solution for this external API?

Next Auth Credentials Provider - SignIn() and Mapping to my API Dto

I am using Next.js and Next Auth to talk to my backend C# ASP.NET API.
My API's response is the following DTO:
{
"data": {
"accessToken": "string",
"refreshToken": "string",
"email": "user#example.com",
"username": "string",
"roles": [
"string"
]
},
"success": true,
"message": "string"
}
I am having a hard time getting that info into the next auth session so that I can grab it with useSession().
I'd also like to be able to display the API "message" to the user in the login form. Incase their account is locked or whatever.
This is what I have:
[...nextauth].js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { API_URL } from "#/constants";
export const authOptions = {
// Configure one or more authentication providers
providers: [
// Add Your Providers Here
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const { usernme, password } = credentials;
const body = JSON.stringify({
username,
password,
});
// Login request to our API.
const res = await fetch(`${API_URL}/Login`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
body: body,
});
const data = await res.json();
// Our API's response contains
/*
{
"data": {
"accessToken": "string",
"refreshToken": "string",
"email": "user#example.com",
"username": "string",
"roles": [
"string"
]
},
"success": true,
"message": "string"
}
*/
const user = {
success: data.success,
message: data.message,
email: data.data.email,
username: data.data.username,
accessToken: data.data.accessToken,
refreshToken: data.data.refreshToken,
roles: data.data.roles,
};
// EVERYTHING TO HERE IS GOOD!
// I CAN GET THE user OBJECT FILLED.
if (res.ok && user) {
return user; //<---- is this actually returning the full user object to the session?
} else {
return null;
}
},
}),
],
pages: { signIn: "/login" },
};
export default NextAuth(authOptions);
Navbar Links:
<Nav.Link as={Link} href='/login' onClick={() => signIn()}>Login</Nav.Link>
<Nav.Link as={Link} href='/signout' onClick={() => signOut({callbackUrl: '/'})}>Signout</Nav.Link>
Login form:
// Get data from the form.
const nextAuthSettings = {
username: event.target.username.value,
password: event.target.password.value,
redirect: true,
callbackUrl: "/dashboard",
};
// Send the form data to Next Auth
const result = await signIn("credentials", nextAuthSettings);
// Error Handling
// THIS DOES NOT WORK
// I don't think signIn() returns a copy of the user object unfortunately...
if (!result.success) {
// Display the API Error Message On The Page
setErrorMsg(result.message);
}
And then in various pages, when I want to access the user object I am doing this :
import { useSession } from "next-auth/react";
const { data: session } = useSession();
// This only shows the email
<span>{session?.user.email}</span>;
// It looks like a JWT or something when I console log it
{
"user": {
"email": "users#email.com"
},
"expires": "2023-03-16T12:39:28.120Z"
}
Any help appreciated!
I need to be able to access the user object my API is returning throughout my app.
At the moment I'm just getting this session.user.email and nothing else ??
it's like I am not mapping the API's response to whatever Next Auth wants me to create...
you have to use callbacks :
callbacks: {
async jwt({ user, token }) {
// update token from user
if (user) {
token.user = user;
}
// return final_token
return token;
},
async session({ session, token }) {
// update session from token
session.user = token.user;
return session;
},
},
Now you can access your session with useSession() and your token with getToken()

How to pass additional parameters in next-auth social login in NextJs?

I need to pass additional parameters to signIn function using next-auth in a NextJs project.
Here is what I tried.
<button
onClick={() =>
signIn(providers.facebook.id, { userType: "customer" })
}
>
<img src="images/facebook.svg" className="w-5 h-5" />
</button>
[...nextAuth].js code
import NextAuth from "next-auth";
import dbConnect from "../../../lib/dbConnect";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "../../../models/User";
import brcypt from "bcryptjs";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import InstagramProvider from "next-auth/providers/instagram";
dbConnect();
export default NextAuth({
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
//The providers are the authentication method
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: "Credentials",
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
try {
const email = credentials.email;
const password = credentials.password;
const user = await User.findOne({ email: email });
if (!user) {
return null;
}
if (user) {
let allow = await signInUser({ password, user });
if (allow == true) {
return user;
} else {
return null;
}
}
} catch (error) {
return null;
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl:
"https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
authorization: {
params: {
userType: "customer" || "admin",
},
},
}),
InstagramProvider({
clientId: process.env.INSTAGRAM_CLIENT_ID,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET,
}),
],
pages: {
signIn: "/login",
},
database: process.env.MONGODB_URI,
callbacks: {
async jwt(token, profile) {
console.log("jwt token>>>>", token);
console.log("jwt profile>>>>", profile);
return token;
},
async session({ session, user, token }) {
if (token) {
const name = token.token.user.name;
const email = token.token.user.email;
const image = token.token.user.image;
const platform = token.token.account.provider;
handleUser(name, email, image, platform);
}
return token.token.token;
},
},
});
const signInUser = async ({ password, user }) => {
let allow = true;
if (!password) {
allow = false;
}
const isMatch = await brcypt.compare(password, user.password);
if (!isMatch) {
allow = false;
}
return allow;
};
async function handleUser(name, email, image, platform) {
console.log("Handle User>>>>>", name);
console.log("Handle email>>>>>", email);
console.log("Handle image>>>>>", image);
console.log("Handle platform>>>>>", platform);
}
Inside the callbacks function I tried logging token & profile. The additional params I passed is not being sent.
What is the right way to achieve this in Next.js?
You can find an article on the GitHub discussion section for next-auth here. The general gist of the answer is that you specify additional custom parameters in the Google Provider parameter (as below). There are other steps, but overall they appear to have solved it. Hope this helps.
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
userType: "user" || "admin", <-- THIS ONE
},
},
}),

How to get accessToeken in NextAuth?

I am trying to implement a traditional authentication setup with NextAuth. The backend is sending a response like the below after a successful login. The library looks excellent but I don't see where the raw accessToken is saved or any way to get this. I need to add it with every request header I send to the backend. How to deal with this kind of authentication? I would be pleased if you spent some of your time helping me in this regard.
Backend Response after login. FYI I can change the response if need
{
"statusCode": 200,
"data": {
"accesstoken": "eyJhbGcicCI6IkpXVCJ9.eyJlbWFpbCI6ImFzaWY.......",
"user": {
"name": "Participant",
"image": "https://i.pravatar.cc/150?img=4",
"email": "asif.saho#gmail.com"
}
}
}
This is what my [...nextauth].ts looks like.
import axios from 'axios';
import NextAuth, { Session, User } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import CredentialsProvider from 'next-auth/providers/credentials';
import { AUTH_CONST } from '../../../constants/authConst';
import { log } from '../../../services/logger';
const providers = [
CredentialsProvider({
type: 'credentials',
id: 'credentials',
name: 'credentials',
credentials: {
email: { label: 'email', type: 'email' },
password: { label: 'password', type: 'password' },
},
authorize: async (_credentials, req) => {
const res: Response = await axios.post(AUTH_CONST.signInBeUrl, {
email: req.query!.email,
password: req.query!.password,
});
if (res.data.statusCode === 200) {
return res.data.data.user;
}
return null;
},
}),
];
const logger = {
error(code: any, metadata: any) {
log.error('next auth', JSON.stringify(code, null, 2));
log.error('next auth', JSON.stringify(metadata, null, 2));
},
warn(code: any) {
log.warn('next auth', JSON.stringify(code, null, 2));
},
debug(code: any, metadata: any) {
log.info('next auth', JSON.stringify({ code, metadata }, null, 2));
},
};
export default NextAuth({
secret: process.env.SECRET,
providers,
debug: process.env.AUTH_DEBUG === 'true',
logger,
pages: {
signIn: AUTH_CONST.signIn,
signOut: AUTH_CONST.signOut,
error: AUTH_CONST.error,
},
});
With NextAuth, you can use the session strategy as jwt. This is the default. The session is saved in a cookie and never persisted anywhere.
session: {
strategy: "jwt"
}
You can add an API handler that accepts the token and sets it to the cookie.
For example
export default function handler(req, res) {
const { token } = req.body;
res.setHeader('Set-Cookie', `token=${token}`);
res.json({});
}
The cookie is attached to all the requests sent to your backend.
// This is an example of how to read a JSON Web Token from an API route
import { getToken } from "next-auth/jwt"
export default async (req, res) => {
const token = await getToken({ req })
if (token) {
// Signed in
console.log("JSON Web Token", JSON.stringify(token, null, 2))
} else {
// Not Signed in
res.status(401)
}
res.end()
}
Example is taken from the NextAuth doc.

How to use next-auth using ldap and prisma

i am using next-auth with ldap to authenticate user name and password. i am able to log/authenticate the user using username and password. but when i can't create user using prisma as await is not allowed inside promise.
this is my [...next-auth].js
`
const ldap = require("ldapjs");
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaClient } from "#prisma/client";
const url = `ldap://${process.env.LDAP_SERVER}`;
const prisma = new PrismaClient();
export default NextAuth({
providers: [
CredentialsProvider({
name: "LDAP",
credentials: {
username: { label: "DN", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials, req) => {
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
const client = ldap.createClient({
url: url,
});
return new Promise((resolve, reject) => {
client.bind(
`${credentials.username}#${process.env.LDAP_DOMAIN}`,
credentials.password,
(error) => {
if (error) {
console.log("Wrong email or password.");
reject("Wrong email or password.");
} else {
console.log("Successfully Logged In");
resolve({
username: credentials.username,
password: credentials.password,
});
}
const filter = `(sAMAccountName=${credentials.username})`;
client.search(
process.env.LDAP_BASE_DN,
{
filter,
scope: "sub",
attributes: [
"mail",
"employeeid",
"title",
"name",
"division",
"department",
"section",
],
},
(err, results) => {
if (err) {
reject(`User ${username} LDAP search error`);
}
const entries = [];
results.on("searchEntry", (entry) => {
entries.push(entry.object);
});
results.on("error", (err) => {
reject("LDAP SEARCH error");
});
results.on("end", (result) => {
if (entries.length == 0) {
reject("Something went wrong. Please try again. (AD)");
}
console.log({ entries });
const searchResult = JSON.stringify(entries[0]);
const adEmployee = JSON.parse(searchResult);
const empId = adEmployee?.employeeID;
const name = adEmployee.name;
console.log(empId);
const newUser= await prisma.user.findUnique({
where:{
oracleId:oracleId
}
})
if(!newUser){
await prisma.user.create({
data:{
oracleId:empId,
fullName:name
}
})
}
});
}
);
}
);
});
},
}),
],
pages: {
signIn: "/auth/sign-in",
},
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.username = user.username;
token.password = user.password;
}
return token;
},
session: async ({ session, token }) => {
if (token) {
session.id = token.id;
session.username = token.username;
}
// console.log(token);
return session;
},
},
debug: process.env.NODE_ENV === "development",
secret: process.env.NEXTAUTH_SECRET,
jwt: {
secret: process.env.NEXTAUTH_SECRET,
encryption: true,
},
});
`
await is not allowed inside promise, where should i call prisma.
Thanks
For this u need to use API endpoint (as prisma is used on server side and cannot be used on client side especially when you pass db url from env also not shown on frontend), your create for example /api/register where:
import { PrismaClient } from '#prisma/client';
import dotenv from 'dotenv'
dotenv.config();
const prisma = new PrismaClient();
const Handler = async (
req,
res
) => {
await prisma.$connect()
const users = await prisma.user.findMany()
//check if user u add is already in db
//if not then
try {
savedUser = await prisma.user.create({ data: new_user });
await prisma.$disconnect()
} catch (error: any) {
await prisma.$disconnect()
// show db error
return res.status(501).json({message: error.message})
}
res.status(200).json({ message: 'User added to db ' + savedUser.name });
}
this is just a simple explanation of what you need to do to make it work, you may add some safety:
const { username, password } = req.body
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required' });
}
then u call api endpoint:
const response = await axios.post(
LOGIN_URL,
JSON.stringify({ username, password }),
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true
}
)
where LOGIN_URL could be /api/register

Resources