Skip to content
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

v6 and Next 13 pages middleware #663

Open
viachaslau-latushkin opened this issue Dec 8, 2023 · 7 comments
Open

v6 and Next 13 pages middleware #663

viachaslau-latushkin opened this issue Dec 8, 2023 · 7 comments

Comments

@viachaslau-latushkin
Copy link

Hi!
Thank you for your work!

I faced with problem in v6: save() and destroy() not working before return in nextjs middleware.
And changes applying only on next tick or after reloading page or on new request to pages/api folder.

"iron-session": "^6.3.1",
"next": "13.2.4"

@Thanaen
Copy link

Thanaen commented Dec 11, 2023

@viachaslau-latushkin ➡️ vercel/next.js#49442 (comment)

@viachaslau-latushkin
Copy link
Author

@viachaslau-latushkin ➡️ vercel/next.js#49442 (comment)

Thank you!
But in this case cookie from middleware is not encrypted.

@Thanaen
Copy link

Thanaen commented Dec 18, 2023

Hello @viachaslau-latushkin ,
iron-session adds the encrypted cookie via the "set-cookie" header, so the applySetCookie method applies the correctly encrypted cookie!

What makes you think otherwise?

@viachaslau-latushkin
Copy link
Author

viachaslau-latushkin commented Dec 18, 2023

@Thanaen in middleware I am trying to apply refresh token logic.
I get a new token and try to rewrite cookie with new encrypted (encrypted token) value.
Yes, applySetCookie really add new cookie from middleware and it exists in next steps.
But how get new encrypted value with help of iron-session which will be encrypted in middleware with help of sessionOptions.password and after successfully decrypt this value (again with help of sessionOptions.password) in time of SSR or in Api Router handler?

@arthurjdam
Copy link

arthurjdam commented Jan 12, 2024

You can use sealData() and unsealData() to encrypt/decrypt the cookie values.

Assuming you're doing an oauth/refresh token kind of thing, here's something I used recently:

import {
  IRON_COOKIE_NAME,
  SessionData,
  ironOptions,
} from './auth';

export async function middleware() {
  const session = await getIronSession<SessionData>(cookies(), ironOptions);
  const next = NextResponse.next();

  if (!session.token?.expires_at) {
    // no/invalid token, send user to auth
    return NextResponse.redirect(getGoogleAuthURL());
  }
  if (session.token.expires_at < Date.now()) {
    // refresh token
    try {
      const token = await refreshToken(session.token);

      const sealed = await sealData(
        {
          ...session,
          token: { ...token, expires_at: Date.now() + token.expires_in * 1000 },
        },
        ironOptions,
      );
      next.cookies.set(IRON_COOKIE_NAME, sealed);
      // await session.save(); // won't work
    } catch (e) {
      // some error while refreshing token, send user to auth
      console.error(e);
      return NextResponse.redirect(getGoogleAuthURL());
    }
  }

  return next;
}

@viachaslau-latushkin
Copy link
Author

@arthurjdam
Thank you.
Nice solution.
And good point to read documentation from start to end.

@MalakJoseph
Copy link

As @arthurjdam solution, here's the updated version that worked for me. Hope it may save time for others.

This solution fixes the error Cookies can only be modified in a Server Action or Route Handler after calling the await session.save().

middleware.ts

export default async function middleware(req: NextRequest) {
  const { isLoggedIn, exp } = await getSession();

  // Refresh Token Rotation
  if (isLoggedIn && exp && exp < Date.now() / 1000) {
    try {
      const newSession = await refreshToken();
      const sealed = await sealData(newSession, sessionOptions);
      const res = NextResponse.redirect(req.url);

      res.cookies.set({
        name: '[your-cookie-name]',
        value: sealed,
      });
      return res;
    } catch (e) {
      await logout();
    }
  }
  
  return NextResponse.next();
}

authActions.ts

export async function getSession() {
  return await getIronSession<User>(cookies(), sessionOptions);
}

export async function refreshToken() {
  const session = await getSession();
  const res = await fetch(
    `${process.env.NEXT_PUBLIC_BACKEND_BASE_URL}/user/refresh`,
    {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + session.refreshToken },
    }
  );
  const { accessToken, refreshToken } = await res.json();
  const newUser = jwt.decode(accessToken) as JwtPayload;

  session.userToken = accessToken;
  session.refreshToken = refreshToken;
  session.exp = newUser.exp;

  return session;
}

P.S. I'm assuming the interface of my User object is:

export interface User {
  id: string;
  isLoggedIn: boolean;
  name: string;
  userToken: string;
  refreshToken: string;
  exp: number | undefined;
}

I'm available for any further clarifications or inquiries...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants