Next.js Discord

Discord Forum

Next.js authorisation & setting cookies

Unanswered
vTotal posted this in #help-forum
Open in Discord
Avatar
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.

// 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

Avatar
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.
Avatar
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)
Avatar
So you're saying that
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
   }
}
Avatar
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 cookies
Avatar
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.
Avatar
oh
Avatar
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: {
Avatar
yeah i wouldnt do that and leave the setting and getting of cookies to next-auth.
Avatar
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
Image
Avatar
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?
Avatar
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.
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
Avatar
thats odd. it shouldn't do that since the NEXTAUTH_SECRET env var is the same
Avatar
Yeah, that would make sense
I mean other functionality continued to work before, I can still make mutations to the server
Avatar
ure saying session always expires after you close your dev server and run it again?
Avatar
That's what I'm understanding
Avatar
thats weird 🤣
Avatar
I've just gone back to an old part of my branch
now granted, this one doesn't have a middleware.js
Avatar
well i could tell you my idea to set cookies in authorize() but its kinda complicated
Avatar
well I guess let's see if we can do this with less complexity first 😂
Avatar
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
Avatar
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
Avatar
so this is not true then
Avatar
Probably not
Avatar
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
Avatar
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 😂
Avatar
you can shorten the jwt duration in nextauth
set it to 10 seconds or smth
jwt: { duration: 30 } iirc
Avatar
in callbacks?
Avatar
no, in authConfig
do you use Typescript?
Avatar
Unfortunately not
not in this project at least
Yeah things go wrong after the expiration of the jwt
Avatar
how bad?
Avatar
I get an issue about not using a session provider, despite wrapping my entire dashboard in a session provider
Avatar
where were you? in a route from app dir or in a route from pages dir?
Avatar
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.
Avatar
the app router can also benefit from SEO but okay but i dont think that is a problem
does <SessionWrapper> exists in pages dir too?
Avatar
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
Avatar
so you didnt do this?
Image
Avatar
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;

}
Avatar
is it because you dont need to do auth stuff in pages dir?
Avatar
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 😂
Avatar
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
Avatar
I was struggling for solutions at this point I think
Avatar
have you tried getServerSession() ?
have you tried your setup in a simpler project?
Avatar
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
Avatar
lol
Avatar
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
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?
Avatar
i would
probably make sure everything works first
before implementing refresh token rotation
Avatar
well from what my console is telling me, it does
up until the jwt expires
my output goes from { company: '1' } which is intended
to { error: 'Internal server error' } after 5 minutes/30 seconds, however long it takes for the token to expire
Avatar
sooo
can you give me the full error message of this?
Avatar
 ⨯ 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
Avatar
like including all of its stack traces
Avatar
 ⨯ ./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
Avatar
ok then can you let me know where authConfig is imported?
Avatar
Auth config is imported at app/api/auth/[...nextAuth]/route.js, and any other route within the auth folder for that matter
Avatar
yes can you actually screenshot the sidebar when you do right click and "Find all Implementations"
Avatar
Image
that doesn't seem right to me
Avatar
its "Find all references" sorry
is it possible for your authConfig to not be imported into pages dir
Avatar
Image
Avatar
there, you signOut button
is it imported into the pages dir?
Avatar
so just move that into app?
no
let me check
but pretty sure it's not
Avatar
no it doesn't matter if you put it into /app if you still import it to pages dir
Avatar
Image
Avatar
one of the imports of authConfig somehow managed to get to pages dir
is my current suspicion
Avatar
Right, and that issue only comes up when we add cookies into the mix
Avatar
yeah
you can either: do that

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
Avatar
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 😂
Avatar
no problem mate