Next.js authorisation & setting cookies
Unanswered
vTotal posted this in #help-forum
vTotalOP
Hi all,
Looking for some help and can't quite get round to a solution in my current project set up. I'm looking to sign my users in with their credentials and generate an authorisation token for them to then add to the cookies of the site. However, I can't seem to find a way to save cookies and then use them after the fact.
The first file I've got of relevance is my standard next auth route. It just takes the auth configuration that I've got and uses it.
Looking for some help and can't quite get round to a solution in my current project set up. I'm looking to sign my users in with their credentials and generate an authorisation token for them to then add to the cookies of the site. However, I can't seem to find a way to save cookies and then use them after the fact.
The first file I've got of relevance is my standard next auth route. It just takes the auth configuration that I've got and uses it.
// app/api/auth/[...nextAuth]/route.js
import NextAuth from "next-auth/next";
import { authConfig } from "@/lib/authConfig";
export const handler = NextAuth(authConfig);
export {handler as GET, handler as POST};
141 Replies
vTotalOP
The next file I have, where my problems are, is a config file. I keep getting errors around importing cookies as it appears it's reading from my pages directory (my project uses both app and pages for separate functionalities) rather than my app directory.
// lib/authConfig.js
import CredentialsProvider from "next-auth/providers/credentials";
// ============================
// Problem below
// ============================
import { cookies } from "next/headers";
export const authConfig = {
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
error: "/login",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {},
password: {}
},
async authorize(credentials, req) {
const { username, password } = credentials;
const loginMutation = `
mutation Login($username: String!, $password: String!) {
login(input: {password: $password, username: $username}) {
authToken
}
}
`;
const response = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + process.env.WORDPRESS_JWT_AUTH_TOKEN
},
body: JSON.stringify({
query: loginMutation,
variables: { username, password }
})
});
const { data } = await response.json();
if (data.login) {
const userQuery = `
query GetUser($username: ID!) {
user(id: $username, idType: ${username.includes("@") ? "EMAIL" : "USERNAME"}) {
username
email
nickname
}
}
`;
const userResponse = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + data.login.authToken
},
body: JSON.stringify({
query: userQuery,
variables: { username: credentials.username }
})
});
const userData = await userResponse.json();
if (userData.data.user) {
// ============================
// How to set cookies here??
// ============================
return {
token: data.login.authToken,
...userData.data.user
}
}
} else {
return null;
}
}
})
],
callbacks: {
jwt: async ({ token, user, trigger, session }) => {
user && (token.user = user)
if(trigger === "update" && session?.email) {
token.user.email = session.email
}
return token
},
session: async ({ session, token }) => {
session.user = token.user
return session
}
}
};
Oh, and here is my middleware.js as I figure that might be important
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
export async function middleware(req) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
console.log(token);
if (token) {
// Check if the token is expired
const expiresAt = new Date(token.exp * 1000);
const now = new Date();
if (expiresAt < now) {
// Refresh the token
const authToken = token.authToken;
const response = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
query: `
mutation RefreshToken($refreshToken: String!) {
refreshJwtAuthToken(input: { jwtRefreshToken: $refreshToken }) {
authToken
}
}
`,
variables: { refreshToken }
})
});
const { data } = await response.json();
if (data.refreshToken) {
// Set new tokens in cookies
const newAuthToken = data.refreshToken.authToken;
const response = NextResponse.next();
response.cookies.set('authToken', newAuthToken, { httpOnly: true });
return response;
}
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*']
};
That's my logic for refreshing tokens if one expires
I just can't get my head around how I access the auth token in a way that allows me to refresh it when it expires and to then run any functions I've got to get user information for when the user is logged in, as this is currently effecting any queries I use with authentication.
Any help would be greatly appreciated.
Seeing that you are using next-auth, your custom provider's authorize()'s function needs ONLY to return User object. The cookie stuff will then be handled by NextAuth and you can get them by modifying the
jwt()
callback to save extra data onto jwt and session()
call to extract data from the jwt to the auth()
function (or getServerSession()
depending what version of nextauth ure using)vTotalOP
So you're saying that
becomes
if (userData.data.user) {
// ============================
// How to set cookies here??
// ============================
return {
token: data.login.authToken,
...userData.data.user
}
}
becomes
if (userData.data.user) {
// ============================
// How to set cookies here??
// ============================
return {
...userData.data.user
}
}
Okay
firstly you don't set cookies in authorize() function. the setting and getting of cookies is usually handled by NextAuth. But if you insist, you can use
await cookies()
from next/headers to set and get cookiesvTotalOP
Well I have tried doing this, but I get an error as it's trying to read my project from the pages directory rather than the app directory I'm using all the authorisation functionality from. To clarify, I've got both a pages and app directory running as this project is both a front-end site and a backend dashboard that requires that auth functionality.
vTotalOP
Error: × You're importing a component that needs "next/headers". That only works in a Server Component which is not supportting-started/
│ react-essentials#server-components
drop\lib\authConfig.js:2:1]
1 │ import CredentialsProvider from "next-auth/providers/credentials";
2 │ import { cookies } from "next/headers";
· ───────────────────────────────────────
3 │
4 │ export const authConfig = {
4 │ session: {
yeah i wouldnt do that and leave the setting and getting of cookies to next-auth.
vTotalOP
Right
Yeah because I can see from the devtools that a session token is being set, but it's not the same one as what's generating from my server
i see
just to clarify: why do you need to set additional token to your client? do you plan on hitting your backend without going through next.js backend?
vTotalOP
I think the issue has kind of just spiraled whilst trying to get some functionality to work.
Essentially the problem I would have is that, during development, if I remained logged into the site then closed the local dev environment and restarted again, certain pieces of data wouldn't refresh properly on my back end.
Essentially the problem I would have is that, during development, if I remained logged into the site then closed the local dev environment and restarted again, certain pieces of data wouldn't refresh properly on my back end.
because my tokens would come back as expired
so I've gone down this rabbithole to try and figure out what the best way to handle my tokens are so that I can make sure they stay active and if they're not active, then to refresh them without causing any issues for the user
thats odd. it shouldn't do that since the NEXTAUTH_SECRET env var is the same
vTotalOP
Yeah, that would make sense
I mean other functionality continued to work before, I can still make mutations to the server
ure saying session always expires after you close your dev server and run it again?
vTotalOP
That's what I'm understanding
thats weird 🤣
vTotalOP
I've just gone back to an old part of my branch
now granted, this one doesn't have a middleware.js
well i could tell you my idea to set cookies in authorize() but its kinda complicated
vTotalOP
well I guess let's see if we can do this with less complexity first 😂
well its just creating a wrapper and Dependency Inject the cookie setter conditionally (Depending if its app dir or pages dir)
if that sounds complicated
vTotalOP
Right okay, think I might have a clarification that could help
It doesn't just immediately expire after I close a dev session
It expires after 5 minutes, which is correct because the token should invalidate after that long
SO
it's not as big as it seems I think
so this is not true then
vTotalOP
Probably not
so let me ask you again then 🤣 : why do you need to set additional token to your client? do you plan on hitting your backend without going through next.js backend? or is it the, after-5-minute mark that things get unfavourable
vTotalOP
I think it's after the 5 minute mark that it all gets funky
appreciate your patience 😂
Yeah I think it's after 5 minutes that things get weird, of course in order to double check that for you I would need to wait 5 minutes 😂
you can shorten the jwt duration in nextauth
set it to 10 seconds or smth
jwt: { duration: 30 }
iircvTotalOP
in callbacks?
no, in authConfig
do you use Typescript?
vTotalOP
Unfortunately not
not in this project at least
Yeah things go wrong after the expiration of the jwt
how bad?
vTotalOP
I get an issue about not using a session provider, despite wrapping my entire dashboard in a session provider
where were you? in a route from app dir or in a route from pages dir?
vTotalOP
Well, the issue is being caused on a button component. That is neither in the pages or app dir
// components/buttons/signOut.js
"use client"
import { signOut, useSession } from "next-auth/react";
import { authConfig } from "@/lib/authConfig";
import styles from "@/styles/components/buttons/signOut.module.css";
const SignOut = ({className}) => {
const {data: session} = useSession();
const handleSignOut = async() => {
const response = await signOut({redirect: true, callbackUrl: authConfig.pages.signIn});
}
return (
<button className={className ? className : styles.btn} onClick={handleSignOut}>Sign Out</button>
)
}
export default SignOut;
Which would make sense as there is no wrapper directly on that for session provider
However that button only ever exists within this overall container
// app/layout.js
import '@/styles/globals.css';
import SessionWrapper from '@/components/contexts/session';
export const metadata = {
title: 'Login',
description: 'Login to your account',
}
export default function RootLayout({ children }) {
return (
<SessionWrapper>
<html lang="en">
<body>{children}</body>
</html>
</SessionWrapper>
)
}
Yeah I appreciate the one route approach, but it's a multi-functional site. It kinda needs both app and pages. Pages for SEO and frontend users, and App for a tool that logged in users can use.
does <SessionWrapper> exists in pages dir too?
vTotalOP
It's not used in the pages dir no
because the session wrapper exists in the components folder, outside of both, it does exist technically speaking
but never runs
it is odd because despite the issues with the jwt regeneration, I can still perform other functions
vTotalOP
This update email script works:
// app/api/auth/updateEmail/route.js
import { authConfig } from "@/lib/authConfig";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
const POST = async (req, res) => {
const { currentEmail, newEmail, confirmEmail } = await req.json();
if(newEmail !== confirmEmail) {
return NextResponse.json({ error: "Emails do not match." }, { status: 400 });
}
try {
const session = await getServerSession(authConfig);
if(!session) {
return NextResponse.json({ success: "You must be signed in to update your email." }, { status: 400 });
}
const user = session.user;
const userData = await checkUserExists(user.username, currentEmail);
const updateEmailQuery = `
mutation UpdateEmail($id: ID!, $newEmail: String!) {
updateUser(input: { id: $id, email: $newEmail }) {
user {
email
}
}
}
`;
const updateEmail = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.WORDPRESS_JWT_AUTH_TOKEN },
body: JSON.stringify({
query: updateEmailQuery,
variables: {
id: userData.id,
newEmail
}
})
});
const updateEmailData = await updateEmail.json();
if(updateEmailData.errors) {
return NextResponse.json({ error: updateEmailData.errors[0].message }, { status: 500 });
}
return NextResponse.json({ success: "Email updated." }, { status: 200});
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
};
export { POST };
const checkUserExists = async (username, currentEmail) => {
const userExistsQuery = `
query UserExists($username: ID!) {
user(id: $username, idType: USERNAME) {
id
email
}
}
`;
const userExists = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.WORDPRESS_JWT_AUTH_TOKEN },
body: JSON.stringify({
query: userExistsQuery,
variables: {
username: username
}
})
});
const userExistsData = await userExists.json();
if(userExistsData.data.user.email !== currentEmail) {
return NextResponse.json({ error: "Current email does not match." }, { status: 400 });
}
return !userExistsData.data.user ? NextResponse.json({ success: "User does not exist." }, { status: 400 }) : userExistsData.data.user;
}
is it because you dont need to do auth stuff in pages dir?
vTotalOP
I did not
No not really, nothing on the front really changes when you're authenticated to the back
nor does it need to
and although this script works, this script doesn't and is causing me issues:
// app/api/auth/getUserCompany/route.js
import { authConfig } from "@/lib/authConfig";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
import { headers } from "next/headers";
const POST = async (req, res) => {
try {
const { id } = await req.json();
const authorization = (await headers()).get('authorization');
const response = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authorization
},
body: JSON.stringify({
query: `
query GetUserCompany($id: ID!) {
user(id: $id, idType: USERNAME) {
pmUserDetails {
company
}
}
}
`,
variables: { id: id },
}),
});
const result = await response.json();
if (result.errors) {
return NextResponse.json({ error: result.errors[0].message }, { status: 500 });
}
return NextResponse.json({ company: result.data.user.pmUserDetails.company }, { status: 200 });
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export { POST };
And I have tried to get the authorisation in the same way between files, but I have to keep changing that method 😂
i dont think thats how you protect your routes with nextAuth
interesting that you ditched NextAuth in this part of the code despite using it everywhere else
vTotalOP
I was struggling for solutions at this point I think
have you tried
getServerSession()
?have you tried your setup in a simpler project?
vTotalOP
Not exactly, no
I did try get server session previously
I think that gets me to a point where I get errors from the server about not having a valid token anymore
vTotalOP
Right?
So I guess from here I just need to recreate my middleware.js file to try and refresh the token?
So this goes from
to
const { id } = await req.json();
const authorization = (await headers()).get('authorization');
const response = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authorization
},
body: JSON.stringify({
query: `
query GetUserCompany($id: ID!) {
user(id: $id, idType: USERNAME) {
pmUserDetails {
company
}
}
}
`,
variables: { id: id },
}),
});
to
const { id } = await req.json();
const session = await getServerSession(authConfig);
const response = await fetch(process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.accessToken}`,
},
body: JSON.stringify({
query: `
query GetUserCompany($id: ID!) {
user(id: $id, idType: USERNAME) {
pmUserDetails {
company
}
}
}
`,
variables: { id: id },
}),
});
which makes sense
and then middleware acts before, so I need to check the token and refresh?
i would
probably make sure everything works first
before implementing refresh token rotation
vTotalOP
well from what my console is telling me, it does
up until the jwt expires
my output goes from
{ company: '1' }
which is intendedto
{ error: 'Internal server error' }
after 5 minutes/30 seconds, however long it takes for the token to expiresooo
can you give me the full error message of this?
vTotalOP
⨯ node_modules\next-auth\react\index.js (91:1) @ useSession
⨯ Error: [next-auth]: `useSession` must be wrapped in a <SessionProvider />
at SignOut (./components/buttons/signOut.js:17:90)
digest: "1987165950"
89 | var value = React.useContext(SessionContext);
90 | if (!value && process.env.NODE_ENV !== "production") {
> 91 | throw new Error("[next-auth]: `useSession` must be wrapped in a <SessionProvider />");
| ^
92 | }
93 | var _ref2 = options !== null && options !== void 0 ? options : {},
94 | required = _ref2.required,
that is the full error
oh wait
wrong one
like including all of its stack traces
vTotalOP
⨯ ./lib/authConfig.js
Error: × You're importing a component that needs "next/headers". That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/
│ react-essentials#server-components
│
│
╭─[C:\Users\Admin\Documents\Projects\rapidrop\lib\authConfig.js:2:1]
1 │ import CredentialsProvider from "next-auth/providers/credentials";
2 │ import { cookies } from "next/headers";
· ───────────────────────────────────────
3 │
4 │ export const authConfig = {
4 │ session: {
╰────
Import trace for requested module:
./lib/authConfig.js
./components/buttons/signOut.js
is it something as minor as moving the signOut button component into the app directory?
I mean it's not in the page directory either
but it just thinks that it is
ok then can you let me know where
authConfig
is imported?vTotalOP
Auth config is imported at
app/api/auth/[...nextAuth]/route.js
, and any other route within the auth folder for that matteryes can you actually screenshot the sidebar when you do right click and "Find all Implementations"
vTotalOP
that doesn't seem right to me
its "Find all references" sorry
is it possible for your authConfig to not be imported into pages dir
vTotalOP
there, you signOut button
is it imported into the pages dir?
no it doesn't matter if you put it into
/app
if you still import it to pages dirvTotalOP
one of the imports of authConfig somehow managed to get to pages dir
is my current suspicion
vTotalOP
Right, and that issue only comes up when we add cookies into the mix
yeah
you can either: do that
or let nextauth handle the refresh tokens
or let nextauth handle the refresh tokens
but nextauth doesn't handle refresh token by default
so you have to see this article https://authjs.dev/guides/refresh-token-rotation
vTotalOP
ooohh, there's a jwt callback
that might be something to look into
I don't know how I didn't click onto that sooner, it's been a long week 😂
wait no
I've already got a jwt callback
I guess I just need to expand it
callbacks: {
jwt: async ({ token, user, trigger, session }) => {
user && (token.user = user)
if(trigger === "update" && session?.email) {
token.user.email = session.email
}
return token
},
session: async ({ session, token }) => {
session.user = token.user
return session
}
}
Right, I will explore this and leave you alone
thank you for all the help @Alfonsus Ardani
and the patience 😂
no problem mate