Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating JWT on backend server and passing that to next-auth #4834

Closed
hagginoaks opened this issue Jul 4, 2022 · 0 comments
Closed

Generating JWT on backend server and passing that to next-auth #4834

hagginoaks opened this issue Jul 4, 2022 · 0 comments
Labels
question Ask how to do something or how something works

Comments

@hagginoaks
Copy link

Question 💬

Hello,

I have an external API server which is running on express.js. This api server needs all of its endpoints secured, so I already create a JWT on the server when a user logs in, and send that back to my next.js application.

When using next-auth, I notice that there is session.maxAge. It seems like this key doesn't respect my server generated JWT maxAge. So if I set my maxAge in [...nextauth].ts to something like 1 minute, and on my backend, the JWT is 2 minutes, next-auth will automatically revoke the token after 1 minute.

I was looking into doing a custom database adapter but it seems like all of those are for doing database operations within the next.js application, and not by something like an external API server. Am I doing this right?

Also is there a good example of an implementation like this already? It would obviously be simpler if all of my logic was contained within a next.js application and I allowed next-auth to manage the JWT age, but it's imperative that my external API tokens stay in sync with my client.

How to reproduce ☕️

[...nextauth].ts:

async function refreshAccessToken(token: any) {
  try {
    const response = await clientFetch({
      url: `${API_SERVER_PATH}${AUTH_API_URL}/token`,
      token: token.refreshToken as string,
      method: 'POST',
    })
    const json = await response.json()

    return {
      ...token,
      accessToken: json.accessToken,
      refreshToken: json.refreshToken ?? json.refreshToken,
      accessTokenExpires: json.accessToken.exp,
    }
  } catch (error) {
    return {
      ...token,
      error: 'RefreshAccessTokenError',
    }
  }
}

const Auth = (req: NextApiRequest, res: NextApiResponse) => {
  return NextAuth(req, res, {
    pages: {
      signIn: '/login',
      newUser: '/signup',
    },
    providers: [
      CredentialsProvider({
        id: 'credentials',
        name: 'Credential Signup',
        async authorize(credentials) {
          const { usernameOrEmail, password }: any = credentials

          try {
            const response = await clientFetch({
              url: `${API_SERVER_PATH}${AUTH_API_URL}/login`,
              data: {
                usernameOrEmail,
                password,
              },
              method: 'POST',
            })
            const json = await response.json()

            if (response.status === 422 || response.status === 401) {
              throw new Error(JSON.stringify(json))
            }

            if (response.status === 200) {
              return json
            }
          } catch (err) {
            throw new Error(err.message)
          }
        },
        credentials: {},
      }),
    ],
    callbacks: {
      async jwt({ token, user }) {
        if (user) {
          token.accessToken = user.accessToken
          token.refreshToken = user.refreshToken
          token.accessTokenExpires = user.accessToken.exp
        }

        if (Math.floor(Date.now() / 1000) > Number(token.exp)) {
          return await refreshAccessToken(token)
        }

        return token
      },

      async session({ session, token }: any): Promise<any> {
        if (token) {
          try {
            const response = await clientFetch({
              url: `${API_SERVER_PATH}${AUTH_API_URL}/me`,
              method: 'GET',
              token: token.accessToken,
            })

            if (response.status !== 200) {
              throw new Error('Unauthorized')
            }

            const json = await response.json()

            return {
              ...session,
              accessToken: token.accessToken,
              accessTokenExpires: token.accessTokenExpires,
              user: json,
            }
          } catch (err) {
            return {}
          }
        }

        return null
      },
    },
    session: {
      strategy: 'jwt',
      maxAge: 60 * 1, // 1 minute test
    },
    secret: process.env.JWT_SECRET,
  })
}

My token service on the server that generates the JWT:

const signToken = (userId: number, exp: number) => {
  const secret = process.env.JWT_SECRET as string
  const payload = {
    user: {
      id: Number(userId),
    },
  }
  const options = {
    expiresIn: exp,
    audience: `${userId}`,
  }

  return JWT.sign(payload, secret, options)
}

export const signAccessToken = (userId: number) => {
  const exp = Math.floor(Date.now() / 1000) + 60 * 2 // 2 minutes

  try {
    return {
      token: signToken(userId, exp),
      exp,
    }
  } catch (err) {
    throw new Error('Could not sign access token')
  }
}

export const signRefreshToken = async (userId: number) => {
  const exp = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60 // 30 days

  try {
    const token = signToken(userId, exp)
    const userToken = await UserToken.findOne({
      where: {
        user_id: userId,
      },
    })

    if (!userToken) {
      console.log('no user token found creating')
      await UserToken.create({
        user_id: userId,
        token: token as string,
      } as Partial<IUser>)
    } else {
      console.log('token found updating')
      await userToken.update({
        token,
      })
    }

    return token
  } catch (err) {
    throw new Error('Could not sign refresh token')
  }
}

And here is my authGuard which is attached to all my endpoints on my express.js server:

server.get(`${AUTH_API_URL}/me`, authGuard(['normal']), auth.getMe)`)
const authGuard = (roles: any) => async (req: any, res: any, next: any) => {
  try {
    let token = req.headers.authorization?.split(' ')[1]

    if (!token) {
      throw new WrappedError(StatusCodes.UNAUTHORIZED)
    }

    // Verify token
    try {
      const decoded: IJWTPayload = JWT.verify(
        token,
        process.env.JWT_SECRET as string
      ) as IJWTPayload

      if (decoded.user?.id) {
        const user: IUser = await User.findByPk(decoded.user.id)

        if (
          !roles.includes(USER_ROLES.NORMAL) &&
          user.role === USER_ROLES.NORMAL &&
          !roles.includes(USER_ROLES.ADMIN) &&
          user.role === USER_ROLES.ADMIN
        ) {
          throw new WrappedError(StatusCodes.UNAUTHORIZED)
        }

        req.user = user
        next()
      }
    } catch (err) {
      throw new WrappedError(StatusCodes.UNAUTHORIZED)
    }
  } catch (err: any) {
    return res.status(err.statusCode || StatusCodes.UNAUTHORIZED).end()
  }
}

Contributing 🙌🏽

Yes, I am willing to help answer this question in a PR

@hagginoaks hagginoaks added the question Ask how to do something or how something works label Jul 4, 2022
@nextauthjs nextauthjs locked and limited conversation to collaborators Jul 5, 2022
@balazsorban44 balazsorban44 converted this issue into discussion #4839 Jul 5, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Ask how to do something or how something works
Projects
None yet
Development

No branches or pull requests

1 participant