next.jsapollo-servernext-auth

Next auth not working properly using Apollo graphql


Hello someone can help me with this problem?

My back end its an Apollo graphql server and this is the auth logic:

const jwt = require("jsonwebtoken");
const isAuth = ({ req }) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    req.isAuth = false;
    return { req };
  }

  const token = authHeader.split(" ")[1];
  if (!token || token === "") {
    req.isAuth = false;
    return { req };
  }
  try {
    const decodedToken = jwt.verify(token, process.env.JSON_WEB_TOKEN_KEY);
    if (!decodedToken) {
      req.isAuth = false;
      return { req };
    }
    req.isAuth = true;
    req.userId = decodedToken.userId;
    req.companyId = decodedToken.companyId;
    if (decodedToken.role === "ADMIN") {
      req.isAdmin = true;
    } else {
      req.isAdmin = false;
    }
    return { req };
  } catch (error) {
    return { req };
  }
};
module.exports = isAuth;

index.js:

const app = express();
const httpServer = http.createServer(app);

// Api server graphql with apollo
async function startServer() {
  const server = new ApolloServer({
    typeDefs,
    resolvers,
    plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
  });
  await server.start();
  app.use(
    "/api",
    cors(),
    bodyParser.json(),
    expressMiddleware(server, {
      context: isAuth,
    })
  );
  await new Promise((resolve) =>
    httpServer.listen({ port: process.env.PORT || 4001 }, resolve)
  );
}

startServer();

This is a endpoint of the back end:

export const getForm = async (_, { id }, { req }) => {
  if (!req.isAuth) {
    throw new Error("Unauthenticated!");
  }
  const createdForm = await prisma.form.findUnique({
    where: { id },
    include: { inputs: true, otherCase: true, routes: true },
  });
  if (!createdForm) {
    throw new Error("Form not found!");
  }
  return createdForm;
};

Finally the login controller:

login: async (_, { email, password }) => {
      const user = await prisma.user.findUnique({
        where: { email: email },
        include: {
          company: true,
        },
      });
      if (!user) {
        throw new Error("User does not exists");
      }
      const isEqual = await bcrypt.compare(password, user.password);
      if (!isEqual) {
        throw new Error("Password is incorrect");
      }
      const token = jwt.sign(
        {
          userId: user.id,
          email: user.email,
          companyId: user.company.id,
          role: user.role,
        },
        process.env.JSON_WEB_TOKEN_KEY,
        { expiresIn: "30m" }
      );
      return {
        userId: user.id,
        companyId: user.company.id,
        token: token,
        tokenExpiration: 1,
      };
    },

The back end its working, and its unathorizing users after 30m (the expiration of the JWT). But the problem I have is with next-auth, I have the following Authoptions:

export const authOptions = {
  providers: [
    CredentialsProvider({
      credentials: {
        email: {},
        password: {},
      },
      async authorize(credentials) {
        const res = await axios({
          method: "POST",
          url: `${process.env.NEXT_PUBLIC_AUTH_URL}/api`,
          data: {
            query: `
                query Login($email: String!, $password: String!) {
                  login(email: $email, password: $password) {
                    userId
                    token
                    tokenExpiration
                    companyId
                  }
                }
              `,
            variables: {
              email: credentials.email,
              password: credentials.password,
            },
          },
        });
        // Validation
        if (res.data.errors) {
          throw new Error(res.data.errors[0].message);
        }
        const user = res.data.data.login;
        if (user) {
          return user;
        }
        return null;
      },
    }),
  ],
  pages: {
    signIn: "/login",
    signOut: "/logout",
  },
  session: {
    strategy: "jwt",
    maxAge: 30 * 60,
  },
  secret: process.env.NEXTAUTH_SECRET,
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.accessToken = user.token;
        token.userId = user.userId; // Add the userId to the token
        // decode the JWT to extract the role and companyId
        const decodedToken = jwt.decode(user.token);
        if (decodedToken && typeof decodedToken === "object") {
          token.role = decodedToken.role;
          token.companyId = decodedToken.companyId;
        }
      }
      return token;
    },
    async session({ session, token, user }) {
      session.accessToken = token.accessToken;
      session.user = {
        id: token.userId, // Add the userId to the session
        role: token.role, // Add the role to the session
        companyId: token.companyId, // Add the companyId to the session
      };
      return session;
    },
  },
};

I already setup all next auth config ands it working, but sometimes, the server answers Unauthenticated and next-auth thinks im authenthicated and its not redirecting me to the login page instead is showing the error message the back end answers (Unauthenticated)

Someone can help me how to fix this? Also I print the session of next-auth when the server answers Unauthenticated next-auth thinks the user is authorized.

import { useSession } from "next-auth/react";
const { data: session, status } = useSession();
console.log(status) // parints: authenticated

Thank you


Solution

  • Answer: In the server, you don't need to add the expiration to the token, that's handled by next-auth.

    But I still recommend adding a refresh token rotation.

    So in my login function I removed the { expiresIn: "30m" } and now its working.