I've working in a project with Next.js (11.1.2) + NextAuth (^4.0.5) + Strapi(3.6.8).
I'm using Next Auth credentials provider and it's working fine. But I need to access a few user information using session, so I tried to do this using jwt and session callbacks.
When I log response from strapi inside authorize(), I receive { jwt:{}, user:{} }, so it's ok.
//[...nextauth.js]
async authorize(credentials, req) {
try {
const { data } = await axios.post(process.env.CREDENTIALS_AUTH_URL, credentials)
if (data) {
//console.log('data: ', data) //this is ok
return data;
}
else {
return null;
}
} catch (e) {
return null;
}
},
But, in jwt callback, when I log token, i'm getting a bizarre obj with {token:{token:{token:{...}}}:
// [...nextauth.js] callback:{ jwt: async (token) => { console.log(token) }}
token: {
token: {
token: {},
user: {
jwt: ...,
user: [Object]
},
account: { type: 'credentials', provider: 'credentials' },
isNewUser: false,
iat: ...,
exp: ...,
jti: ...
}
}
And account and user is always undefined inside that callbacks.
Finally, when I get session from useSession in a page, I get this:
// console.log(session) in any page
{
session: {
expires: "2022-01-12T19:27:53.429Z"
user: {} // empty
},
token:{
token:{
account: {type: 'credentials', provider: 'credentials'}
exp: ...
iat: ...
isNewUser: false
jti: "..."
token: {} // empty
user: { //exactly strapi response
jwt:{...}
user:{...}
}
}
}
}
All examples I've found aren't handling this objects with this confuse structure and I don't know if I missing something out. Can you help me?
This is my [...nextauth].js:
import NextAuth from "next-auth"
import CredentialsProvider from 'next-auth/providers/credentials'
import axios from 'axios';
export default NextAuth({
providers: [
CredentialsProvider({
name: '...',
credentials: {
email: {label: "Email", type: "text", placeholder: "email#provider.com"},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
try {
const { data } = await axios.post(process.env.CREDENTIALS_AUTH_URL, credentials)
if (data) {
//console.log('data: ', data)
return data;
}
else {
return null;
}
} catch (e) {
return null;
}
},
})
],
secret: process.env.SECRET,
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60 // 30 days
},
jwt: {
secret: process.env.JWT_SECRET,
encryption: true,
},
callbacks: {
jwt: async (token, account) => {
console.log('### JWT CALLBACK ###')
console.log('token: ', token)
console.log('account: ', account)
return token;
},
session: async (session, token, user) => {
console.log('### SESSION CALLBACK ###')
console.log('session: ', session)
console.log('user: ', token)
console.log('user: ', user)
return session;
}
},
pages: {
signIn: '/signin',
signOut: '/signin',
error: '/signin'
}
})
try pls:
- session(session, tokenOrUser)
+ session({ session, token, user })
- jwt(token, user, account, OAuthProfile, isNewUser)
+ jwt({ token, user, account, profile, isNewUser })
https://next-auth.js.org/getting-started/upgrade-v4#callbacks
Related
im building a nextjs application using next-auth. For now i have login with google, credentials and github.If i make login with google that contain the email "example#gmail.com" and then i logout, if i try make login with github but with a account that have the same email "example#gmail.com", i get the error: OAuthAccountNotLinked
In the database model that next-auth provides to us, User have relation with Accounts that is an array, so i think that would be possible have the sabe user but liked with 2 accounts. I forgot something or this is the default behavior?
My [...nextAuth].ts code
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const data = requestWrapper(req, res);
return await NextAuth(...data);
};
export default handler;
export function requestWrapper(
req: NextApiRequest,
res: NextApiResponse
): [req: NextApiRequest, res: NextApiResponse, opts: NextAuthOptions] {
const generateSessionToken = () => randomUUID();
const fromDate = (time: number, date = Date.now()) =>
new Date(date + time * 1000);
const adapter = PrismaAdapter(prisma);
const opts: NextAuthOptions = {
// Include user.id on session
adapter: adapter,
pages: {
signIn: "/login",
},
callbacks: {
session({ session, user }) {
console.log("AAAA");
if (session.user) {
session.user = user;
}
return session;
},
async signIn({ user, account, profile, email, credentials }) {
// Check if this sign in callback is being called in the credentials authentication flow. If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
if (
req.query.nextauth?.includes("callback") &&
req.query.nextauth?.includes("credentials") &&
req.method === "POST"
) {
if (user) {
const sessionToken = generateSessionToken();
const sessionMaxAge = 60 * 60 * 24 * 30; //30Daysconst sessionMaxAge = 60 * 60 * 24 * 30; //30Days
const sessionExpiry = fromDate(sessionMaxAge);
await adapter.createSession({
sessionToken: sessionToken,
userId: user.id,
expires: sessionExpiry,
});
const cookies = new Cookies(req, res);
cookies.set("next-auth.session-token", sessionToken, {
expires: sessionExpiry,
});
}
}
return true;
},
},
jwt: {
encode: async ({ token, secret, maxAge }) => {
if (
req.query.nextauth?.includes("callback") &&
req.query.nextauth.includes("credentials") &&
req.method === "POST"
) {
const cookies = new Cookies(req, res);
const cookie = cookies.get("next-auth.session-token");
if (cookie) return cookie;
else return "";
}
// Revert to default behaviour when not in the credentials provider callback flow
return encode({ token, secret, maxAge });
},
decode: async ({ token, secret }) => {
if (
req.query.nextauth?.includes("callback") &&
req.query.nextauth.includes("credentials") &&
req.method === "POST"
) {
return null;
}
// Revert to default behaviour when not in the credentials provider callback flow
return decode({ token, secret });
},
},
// Configure one or more authentication providers
secret: process.env.NEXTAUTH_SECRET,
// debug: true,
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
profile(profile, token) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
image: profile.avatar_url,
email: profile.email,
role: Role.USER,
};
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID as string,
clientSecret: process.env.GOOGLE_SECRET as string,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
CredentialProvider({
name: "CredentialProvider",
credentials: {
email: { label: "Email", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
async authorize(credentials: any, _req): Promise<any | null> {
const userInputs = {
email: credentials.email,
password: credentials.password,
};
const { user } = await loginCredentials(userInputs);
if (user) {
return user;
} else {
return null;
}
},
}),
],
};
return [req, res, opts];
}
I am using next auth in my next.js project,
but during development, if the server refreshes then the sessio immediately loses the data in it, also if I change to another tab in the browser and then return back to it, the session loses its data, and I will be forced to sign out then sign in to let it work again.
This doesn't happen in production mode.
below it the [...nextauth].js file code:
import NextAuth from "next-auth/next";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import axios from "axios";
let userInfo = {};
export default NextAuth({
site: process.env.SITE,
providers: [
CredentialsProvider({
name: "Credentials",
async authorize(credentials, req) {
let status;
let user = {};
let message = ''
await axios
.post(
`${process.env.NEXT_PUBLIC_DOMAIN_URL}/app/auth`,
{ userName: credentials.username.replace(/\s/g, ''), pwd: credentials.password },
{
headers: { "Content-Type": "application/json" },
withCredentials: true,
}
)
.then(function (response) {
if (response?.data) {
user = { ...response.data };
userInfo = user;
}
})
.catch((error) => {
if (error.response) {
throw new Error(JSON.stringify({ errors: error?.response?.data?.message, status: false }))
} else if (error.request) {
throw new Error(JSON.stringify({ errors: 'Server error, please try again.', status: false }))
} else {
throw new Error(JSON.stringify({ errors: 'Network error, please try again.', status: false }))
}
});
return user;
},
}),
],
pages: {
signIn: "/Auth/Login",
},
session: {
jwt: true,
strategy: "jwt"
},
jwt: {
maxAge: 60 * 60 * 24 * 30 * 6,
},
secret: process.env.NEXT_AUTH_SECRET,
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
user && (token = { ...token, ...user });
return token;
},
async session({ session, token }) {
if (token) {
session.user = { ...userInfo };
}
return session;
},
},
});
Any help please?
I want to render data in client side below from parse after signin from next auth using useSession
{
objectId: 'nttMcKIOQJ',
username: 'fiik346',
email: 'fiik346#gmail.com',
createdAt: '2022-06-26T07:56:41.888Z',
updatedAt: '2022-06-26T07:56:41.888Z',
ACL: { '*': { read: true }, nttMcKIOQJ: { read: true, write: true } },
sessionToken: 'r:2f2c569be3b5120f3893834c2ace7b67'
}
But it's just render email
{"user": {"email":"fiik346#gmail.com"},"expires":"2022-07-26T09:01:41.163Z"}
"authenticated"
This is my callbacks code
// /pages/api/auth/[...nextauth].js
...
callbacks: {
async session({ session, token, user }) { // Send properties to the client, like an access_token from a provider. return session
}, async jwt({ token, user, account, profile, isNewUser }) { return token
} },
...
And client side code
import { useSession } from 'next-auth/react'
import { getToken } from 'next-auth/jwt'
export default function accountIndex() {
const {data: session, status} = useSession() return ( <div>
<h1>Account is {status}</h1>
<pre className="overflow-auto">
{JSON.stringify(session)}
<br/>
{JSON.stringify(status)}
</pre>
</div>
)
}
I don't know how to change my callbacks code to display data from database
callbacks: {
jwt: async ({token, user}) => {
if (user) {
token.data = user
}
return token
},
session: async ({session, token}) => {
if (token.data) {
session.user = token.data
}
return session
}
}
After a while and try harder, I was successful to render data. Just change callback code like below.
...
callbacks: {
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
session.accessToken = token.accessToken
session.user = token.data
return session
},
async jwt({ token, user, account, profile, isNewUser }) {
if (user) {
token.data = user
}
return token
}
},
...
I am using Next-Auth Credentials provider to authenticate using our existing API.
When I follow the directions on https://next-auth.js.org/configuration/callbacks
like this:
callbacks: {
async jwt({ token, user }) {
if (user) {
token.accessToken = user.jwt
}
return token
},
async session({ session, token, user }) {
session.accessToken = token.accessToken
return session
}
}
the resulting session object from useSession() looks like this:
{
expires: "2022-03-22T18:29:02.799Z",
user: {email: 'john#nextIsGreat.com'}
}
I can't use that as it does not have the token available.
So I was able to make up my own working solution, but it is kind of strange because of the way things are grouped together. Here is what I am doing now, that I am trying to figure out how to do better. I use comments to point out the problem areas:
[...nextauth].js:
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import axios from 'axios'
export default NextAuth({
providers: [
Credentials({
name: 'Email and Password',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' }
},
authorize: async (credentials) => {
const url = process.env.API_URL + '/authenticate'
const result = await axios.post(url, {
username: credentials.username,
password: credentials.password
})
const user = result.data
console.log(user)
//It logs this:
/*
{
jwt: 'eyJhbasU1OTJ9.NQ356H4Odya62KmN...', //<---***This is the token i pass in to all of my API calls****
user: {
userId: 207,
email: 'john#nextIsGreat.com',
firstName: 'John',
lastName: 'Doe',
roleId: 1,
}
}
*/
if (user) {
return Promise.resolve(user)
} else {
return Promise.resolve(null)
}
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
if (user.jwt) {
token = { accessToken: user.jwt, restOfUser: user.user }
}
}
return token
},
async session(seshProps) {
return seshProps
}
}
})
Home.js:
export const Home = () => {
const { data: session } = useSession()
console.log(session)
//LOGS THIS --->
/*
{
"session": { "user":{}, "expires":"2022-03-22T17:06:26.937Z"},
"token":{
"accessToken":"eyJ...",
"iat":1645376785,
"exp":1647968785,
"jti":"41636a35-7b9a-42fd-8ded-d3dfgh123455a"
"restOfUser": {
"userId":207,
"email":"john#nextIsGreat.com",
"firstName":"John",
"lastName":"Doe",
"roleId":1
}
}
{
*/
const getPosts=()=> {
const url = 'localhost:4000/posts'
const {data} = axios.get(url, {
Authorization: session.token.accessToken <--**This is the way I am calling my API
})
console.log(data)
}
return (
<div onClick={getPosts}>
Hello, {session.token.restOfUser.firstName}
/* I have to access it like this now, which seems wrong ***** */
</div>
)
}
Cheers for creating your own solution but you do not need it. NextAuth CredentialsProvider handles it already by setting your NextAuth session configuration to session: {strategy: "jwt", ... }.
You can also remove your callbacks for jwt() and session() and remove your owned generated JWT access token. As you do not need it, this way you can authenticate your existing system.
And at your CredentialsProvider({authorize(){}} authorize method. If you had directly connected to the user database, you can directly look up the user credential without doing a post request since it is already considered a server-side function.
I implemented this in next-auth following some tutorial online
import NextAuth from "next-auth"
import Providers from "next-auth/providers";
const https = require('https');
export default NextAuth({
providers: [
Providers.Credentials({
name: 'Credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
const url = 'https://localhost/auth';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify(credentials),
agent: httpsAgent,
headers: {
"Content-Type": "application/json"
}
})
const user = await res.json();
if (res.ok && user) {
return user;
} else {
return null;
}
}
}),
// ...add more providers here
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
if (user?.type) {
token.status = user.type
}
if (user?.username) {
token.username = user.username;
}
return token
},
async session(session, token) {
session.type = token.type;
session.username = token.username;
return session
}
}
})
pretty standard. https://localhost/auth return an object like this (I called it user for now)
{
token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2MzY0MTE4NjEsImV4cCI6MTYzNjQxNTQ2MSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiZXJuYTM5QHdlYmVyLmNvbSJ9.Abenx1GhB-_d9LVpLfa2NYp62Lbw6U65EUQowA0jA_aykx1m-BlBR_YBcL4XIJsknJ99NN8Ees4Zxdsphfhjs7du4TR2MgTITHYy-BYjBX9CsluVSBpm-L7c-oK5vu70eumAy1ixy5MKOTN2EQYCm65RszSheIwZ4LN8vSuzxzZuLszRG9nbpauiHDpYCeLrNeNkz4lhTicfWkdPafR8vhqt4MIeCl-kxbMqc35UNmglzE7n-b9zVh4OhU7bSCoPKZySL5c4GSf7UFFD-mXIe6s9b4qYSXJuLpdspFJSgP7UoEGP1gh8fTb5MDZREYyZOpK3BMU8EdwokngVR9zrbw'
}
I would like to know how to store this token to be used in further calls to my API. I can see the token object in the session callback is
{ iat: 1636411862, exp: 1639003862 }
so next-aut is not doing this for me. Should I set an httpOnly cookie in the session callback? or right after
if (res.ok && user) {
just before to return user?
I found a way just updating the callbacks:
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
if (user?.token) {
token.token = user.token;
}
return token;
},
async session(session, token) {
return session;
}
}
in this way the token from the API is now stored in a httpOnly cookie called __Secure-next-auth.session-token (assuming the token from the API is in the format like above).
If you store the JWT in the cookies so every time you're calling your API you could check the cookie header to see if you have it.