NextAuth - Handle refresh token rotation (Reddit) with database

Viewed 66

Reddit's access token has an expiration of 1 hour, but I want users that log in to my app to be able to post comments on Reddit for example. This means I need to refresh their access token once it has expired. Since I'm using a database (PlanetScale + Prisma) and not a JWT strategy, the documentation found here https://next-auth.js.org/tutorials/refresh-token-rotation is not useful to me (jwt callback is never called).

As far as I'm understanding it, it means it's not really possible to check the expiration in the session callback and refresh the token here without accessing the database each time?

What can I do if I want to refresh the access token in my database? Should I use a JWT strategy instead, even though I'm using a database?

2 Answers

To do refresh token rotation when using a database strategy you can do something like this:

async function refreshAccessToken(session: Session) {
  if (!session.user?.id) {
    return;
  }

  const {
    id,
    refresh_token: refreshToken,
    expires_at: expiresAt,
  } = (await prisma.account.findFirst({
    where: { userId: session.user.id, provider: "reddit" },
  })) ?? {};

  if (!id || !refreshToken) {
    return;
  }

  // If expired refresh it
  if (expiresAt && Date.now() / 1000 > expiresAt) {
    const authorizationString = Buffer.from(
      `${process.env?.["REDDIT_CLIENT_ID"]}:${process.env?.["REDDIT_CLIENT_SECRET"]}`,
    ).toString("base64");

    const headers = {
      Authorization: `Basic ${authorizationString}`,
      "Content-Type": "application/x-www-form-urlencoded",
    };

    const urlSearchParams = new URLSearchParams();
    urlSearchParams.append("grant_type", "refresh_token");
    urlSearchParams.append("refresh_token", refreshToken);
    urlSearchParams.append("redirect_uri", `${process.env?.["NEXTAUTH_URL"]}/api/auth/callback/reddit`);

    const { data } = await axios.post<RedditResponse>("https://www.reddit.com/api/v1/access_token", urlSearchParams, {
      headers,
    });

    await prisma.account.update({
      where: { id },
      data: {
        access_token: data.access_token,
        expires_at: Math.floor(Date.now() / 1000) + data.expires_in,
        refresh_token: data.refresh_token,
        token_type: data.token_type,
        scope: data.scope,
      },
    });
  }
}

You can use this anywhere I guess. I don't know if it makes sense to use this in the session callback or not since it's probably a performance hit, so maybe just call it each time you actually need the access token for something? I'm not knowledgable about this to know what the best practice is in this regard...

After many hours of tinkering i just found out how to get the refresh token into the database!

following the first part of the next auth token refresh tutorial, add the authorization param to the provider options

const GOOGLE_AUTHORIZATION_URL =
  "https://accounts.google.com/o/oauth2/v2/auth?" +
  new URLSearchParams({
    prompt: "consent",
    access_type: "offline",
    response_type: "code",
  });

and

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
      authorization: GOOGLE_AUTHORIZATION_URL,
    }),

This will send me well on my way to figuring out the rest of the process... hope it works for you too!

Related