firebasefirebase-authenticationserver-side-rendering

How to reconcile Firebase Auth token refreshing with Server-Side Rendering


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:

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!


Solution

  • 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:

    // 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...
    }
    
    // /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.