We're using Firebase in a Next.js app at work. I'm new to both, but did my best to read up on both. My problem is more with Firebase, not so much with Next.js. Here's the context:
In the client app, I make some calls to our API, passing a JWT (the ID token) in an Authorization header. The API calls admin.auth().verifyIdToken to check that the ID token is fresh enough. This works fine, since I am more or less guaranteed that the ID token gets refreshed regularly (through the use of onIDTokenChanged (doc link)
Now I want to be able to Server-Side Render my app pages. In order to do that, I store the ID token in a cookie readable by the server. But from here on, I have no guarantee that the ID token will be fresh enough next time the user loads the app through a full page load.
I cannot find a server-side equivalent of onIDTokenChanged.
This blog post mentions a google API endpoint to refresh a token. I could hit it from the server and give it a refresh token, but it feels like I'm stepping out of the Firebase realm completely and I'm worried maintaining an ad-hoc system will be a burden.
So my question is, how do people usually reconcile Firebase auth with SSR? Am I missing something?
Thank you!
I've had that same problem recently, and I solved by handling it myself. I created a very simple page responsible for forcing firebase token refresh, and redirecting user back to the requested page. It's something like this:
exp value after extracting it from cookies (If you're using firebase-admin on that server, it will probably tell you as an error after verifying it)// Could be a handler like this
const handleTokenCookie = (context) => {
  try {
    const token = parseTokenFromCookie(context.req.headers.cookie)
    await verifyToken(token)
  } catch (err) {
    if (err.name === 'TokenExpired') {
      // If expired, user will be redirected to /refresh page, which will force a client-side
      // token refresh, and then redirect user back to the desired page
      const encodedPath = encodeURIComponent(context.req.url)
      context.res.writeHead(302, {
        // Note that encoding avoids URI problems, and `req.url` will also
        // keep any query params intact
        Location: `/refresh?redirect=${encodedPath}`
      })
      context.res.end()
    } else {
      // Other authorization errors...
    }
  }
}
This handler can be used on the /pages, like this
// /pages/any-page.js
export async function getServerSideProps (context) {
  const token = await handleTokenCookie(context)
  if (!token) {
    // Token is invalid! User is being redirected to /refresh page
    return {}
  }
  // Your code...
}
/refresh page, responsible for forcing firebase token refresh on client-side, and after both token and cookie are updated, it should redirect user back to the desired page.// /pages/refresh.js
const Refresh = () => {
  // This hook is something like https://github.com/vercel/next.js/blob/canary/examples/with-firebase-authentication/utils/auth/useUser.js
  const { user } = useUser()
  React.useEffect(function forceTokenRefresh () {
    // You should also handle the case where currentUser is still being loaded
    currentUser
      .getIdToken(true) // true will force token refresh
      .then(() => {
        // Updates user cookie
        setUserCookie(currentUser)
        // Redirect back to where it was
        const decodedPath = window.decodeURIComponent(Router.query.redirect)
        Router.replace(decodedPath)
      })
      .catch(() => {
        // If any error happens on refresh, redirect to home
        Router.replace('/')
      })
  }, [currentUser])
  return (
    // Show a simple loading while refreshing token?
    <LoadingComponent />
  )
}
export default Refresh
Of course it will delay the user's first request if the token is expired, but it ensures a valid token without forcing user to login again.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With